【问题标题】:Why doesn't the timer get garbage collected?为什么计时器不收集垃圾?
【发布时间】:2022-01-12 03:13:22
【问题描述】:

我写了一个小实验来更好地理解垃圾收集。场景是这样的:计时器在设定的时间间隔内调用重复事件。没有对象持有指向计时器的指针。当 GC 发生时,计时器是否停止调用其事件?

我的假设是,如果有足够的时间和 GC 尝试,计时器将停止调用其事件。

这是我为测试而编写的代码:

using System;
using System.Threading;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace UnitTest;  


[TestClass]
public class TestGC
{

    //starts a repeating timer without holding on to a reference to the timer
    private void StartTimer(Action callback, int interval)
    {
        var timer = new Timer(t => callback());
        timer.Change(interval, interval);
        timer = null;//this probably does nothing, but lets just overwrite the pointer for good measure
    }



    [TestMethod]
    public void TestEventInvoker()
    {
        var count = 0;
        var interval = 100;//time between timer events (ms)
        var totalTime = 50000;//time of the experiment (ms)
        var expectedCalls = totalTime / interval;//minimum number of times that the timer event is invoked if it isn't stopped
        StartTimer(()=>Interlocked.Increment(ref count),interval);

        //gc periodically to make sure the timer gets GC'ed
        for (int i = 0; i < expectedCalls; i++)
        {
            GC.Collect();
            Thread.Sleep(interval);
        }

        //for debugging
        Console.WriteLine($"Expected {expectedCalls} calls. Actual: {count}");

        //test passes if the timer stops before the time is over, and the minimum number of calls is not achieved
        // the -1 accounts for the edge case where the test completes just before the last timer call had a chance to be executed
        Assert.IsTrue(count < (expectedCalls - 1));
    }


}

我发现计时器继续重复调用其事件,并且增加总时间和 GC 调用次数对停止计时器没有影响。示例输出:

Assert.IsTrue 失败。
预计有 500 次调用。实际:546

所以我的问题是:
为什么计时器会继续触发?
计时器是否被 GC 处理?为什么/为什么不?
如果没有,谁有指向计时器的指针?

我找到的最接近的问题是this one,但答案说System.Threading.Timer 应该在这些情况下被 GC'ed。我发现它没有被 GC'ed。

【问题讨论】:

  • Dispose()它。
  • @rfmodulator 当然可以,如果我的目标是停止计时器。但这不是问题的重点。我想弄清楚为什么计时器没有被 GC 处理
  • @rfmodulator 的评论确实回答了这个问题,不是吗?文档说你必须处理一些东西让 GC 知道它可以被清除。 Dispose
  • 如果你不 Dispose 它,它将需要由 GC 完成。在调用 Collect 之后尝试调用 GC.WaitForPendingFinalizers。但是,也扔掉它
  • System.Threading.Timer 是一些非托管计时器的托管包装器。如果不调用Dispose,托管计时器的实例将被 GC 处理,但其非托管资源仍保持活动状态。

标签: c# timer garbage


【解决方案1】:

System.Threading.Timer 是一些非托管计时器的托管包装器。
如果不调用 Dispose,托管计时器的实例将被 GC 处理,但其非托管资源仍保持活动状态。

【讨论】:

    【解决方案2】:

    当您创建 Timer 时,内部会根据您的 Timer 创建 TimerQueueTimerTimer 对此有一个引用,以便您可以继续修改和控制计时器。

    System.Threading.TimerQueue 类 (Source) 的静态字段 s_queue 包含对活动 TimerQueueTimer 的引用(它是间接的,还有另一个包装类),即使您忘记了对 Timer 的引用它创造了它。

    如果您检查该链接文件中Timer 构造函数及其change 方法的源代码,您将看到在change 期间引用TimerQueueTimer(间接地,队列属于另一个包装类) 确实存储在TimerQueue.s_queue

    【讨论】:

    • 我使用的是 .NET 6,因此源代码无效。我一直在使用反射在TimerQueue 实例之一中寻找对Timer 的引用,但没有取得任何成功。你知道谁指向 .NET 6 中的 Timer 吗?
    • @NigelBess - Timer 本身并没有真正做任何与时间相关的事情——它只是创建另一个内部定义的对象,并将你用它做的所有事情转发给那个对象。因此,对 Timer 实例的 GC 不会删除该其他对象,除非它也没有其他引用剩余。我认为这个实现在 .NET 的较新版本中没有显着变化(但它可能有)。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-07
    • 1970-01-01
    • 1970-01-01
    • 2010-12-19
    • 2014-08-08
    相关资源
    最近更新 更多