为什么计时器不收集垃圾?
Why doesn't the timer get garbage collected?
我写了一个小实验来更好地理解垃圾回收。场景是这样的:计时器在设定的时间间隔内调用重复事件。没有对象持有指向计时器的指针。当 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 中? Why/why不是吗?
如果没有,谁有定时器的指针?
我找到的最接近的问题是 this one,但答案是 System.Threading.Timer
应该 在这种情况下进行 GC。我发现它 不是 被 GC。
System.Threading.Timer
是一些非托管计时器的托管包装器。
在不调用 Dispose
的情况下,托管计时器的实例将被 GC 处理,但其非托管资源仍保持活动状态。
当您创建 Timer
时,内部会根据您的 Timer
创建一个 TimerQueueTimer
。 Timer
对此有引用,以便您可以继续修改和控制计时器。
System.Threading.TimerQueue
class (Source) 的静态字段 s_queue
持有对活动 TimerQueueTimer
的引用(它是间接的,还有另一个包装器 class) 即使您忘记了对创建它的 Timer
的引用。
如果您检查该链接文件中的 Timer
构造函数及其 change
方法的源代码,您将看到在 change
期间对 TimerQueueTimer
的引用(间接地,队列是另一个包装器 class)确实存储在 TimerQueue.s_queue
.
中
我写了一个小实验来更好地理解垃圾回收。场景是这样的:计时器在设定的时间间隔内调用重复事件。没有对象持有指向计时器的指针。当 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 中? Why/why不是吗?
如果没有,谁有定时器的指针?
我找到的最接近的问题是 this one,但答案是 System.Threading.Timer
应该 在这种情况下进行 GC。我发现它 不是 被 GC。
System.Threading.Timer
是一些非托管计时器的托管包装器。
在不调用 Dispose
的情况下,托管计时器的实例将被 GC 处理,但其非托管资源仍保持活动状态。
当您创建 Timer
时,内部会根据您的 Timer
创建一个 TimerQueueTimer
。 Timer
对此有引用,以便您可以继续修改和控制计时器。
System.Threading.TimerQueue
class (Source) 的静态字段 s_queue
持有对活动 TimerQueueTimer
的引用(它是间接的,还有另一个包装器 class) 即使您忘记了对创建它的 Timer
的引用。
如果您检查该链接文件中的 Timer
构造函数及其 change
方法的源代码,您将看到在 change
期间对 TimerQueueTimer
的引用(间接地,队列是另一个包装器 class)确实存储在 TimerQueue.s_queue
.