似乎 System.Timers.Timer
实例通过某种机制保持活动状态,但 System.Threading.Timer
实例不是。
带有周期性 System.Threading.Timer
和自动重置 System.Timers.Timer
的示例程序:
class Program
{
static void Main(string[] args)
{
var timer1 = new System.Threading.Timer(
_ => Console.WriteLine("Stayin alive (1)..."),
null,
0,
400);
var timer2 = new System.Timers.Timer
{
Interval = 400,
AutoReset = true
};
timer2.Elapsed += (_, __) => Console.WriteLine("Stayin alive (2)...");
timer2.Enabled = true;
System.Threading.Thread.Sleep(2000);
Console.WriteLine("Invoking GC.Collect...");
GC.Collect();
Console.ReadKey();
}
}
当我运行这个程序(.NET 4.0 Client,Release,在调试器之外)时,只有 System.Threading.Timer
被 GC'ed:
Stayin alive (1)...
Stayin alive (1)...
Stayin alive (2)...
Stayin alive (1)...
Stayin alive (2)...
Stayin alive (1)...
Stayin alive (2)...
Stayin alive (1)...
Stayin alive (2)...
Invoking GC.Collect...
Stayin alive (2)...
Stayin alive (2)...
Stayin alive (2)...
Stayin alive (2)...
Stayin alive (2)...
Stayin alive (2)...
Stayin alive (2)...
Stayin alive (2)...
Stayin alive (2)...
编辑:我在下面接受了约翰的回答,但我想稍微解释一下。
运行上面的示例程序时(在 Sleep
处有一个断点),这里是有问题的对象和 GCHandle
表的状态:
!dso
OS Thread Id: 0x838 (2104)
ESP/REG Object Name
0012F03C 00c2bee4 System.Object[] (System.String[])
0012F040 00c2bfb0 System.Timers.Timer
0012F17C 00c2bee4 System.Object[] (System.String[])
0012F184 00c2c034 System.Threading.Timer
0012F3A8 00c2bf30 System.Threading.TimerCallback
0012F3AC 00c2c008 System.Timers.ElapsedEventHandler
0012F3BC 00c2bfb0 System.Timers.Timer
0012F3C0 00c2bfb0 System.Timers.Timer
0012F3C4 00c2bfb0 System.Timers.Timer
0012F3C8 00c2bf50 System.Threading.Timer
0012F3CC 00c2bfb0 System.Timers.Timer
0012F3D0 00c2bfb0 System.Timers.Timer
0012F3D4 00c2bf50 System.Threading.Timer
0012F3D8 00c2bee4 System.Object[] (System.String[])
0012F4C4 00c2bee4 System.Object[] (System.String[])
0012F66C 00c2bee4 System.Object[] (System.String[])
0012F6A0 00c2bee4 System.Object[] (System.String[])
!gcroot -nostacks 00c2bf50
!gcroot -nostacks 00c2c034
DOMAIN(0015DC38):HANDLE(Strong):9911c0:Root: 00c2c05c(System.Threading._TimerCallback)->
00c2bfe8(System.Threading.TimerCallback)->
00c2bfb0(System.Timers.Timer)->
00c2c034(System.Threading.Timer)
!gchandles
GC Handle Statistics:
Strong Handles: 22
Pinned Handles: 5
Async Pinned Handles: 0
Ref Count Handles: 0
Weak Long Handles: 0
Weak Short Handles: 0
Other Handles: 0
Statistics:
MT Count TotalSize Class Name
7aa132b4 1 12 System.Diagnostics.TraceListenerCollection
79b9f720 1 12 System.Object
79ba1c50 1 28 System.SharedStatics
79ba37a8 1 36 System.Security.PermissionSet
79baa940 2 40 System.Threading._TimerCallback
79b9ff20 1 84 System.ExecutionEngineException
79b9fed4 1 84 System.StackOverflowException
79b9fe88 1 84 System.OutOfMemoryException
79b9fd44 1 84 System.Exception
7aa131b0 2 96 System.Diagnostics.DefaultTraceListener
79ba1000 1 112 System.AppDomain
79ba0104 3 144 System.Threading.Thread
79b9ff6c 2 168 System.Threading.ThreadAbortException
79b56d60 9 17128 System.Object[]
Total 27 objects
正如 John 在回答中指出的那样,两个计时器都在 GCHandle
表中注册了它们的回调 (System.Threading._TimerCallback
)。正如 Hans 在他的评论中指出的那样,完成此操作后,state
参数也会保持活动状态。
正如 John 所指出的,System.Timers.Timer
保持活动状态的原因是因为它被回调引用(它作为 state
参数传递给内部 System.Threading.Timer
);同样,我们的 System.Threading.Timer
被 GC 的原因是它的回调没有引用它。
添加对 timer1
回调的显式引用(例如,Console.WriteLine("Stayin alive (" + timer1.GetType().FullName + ")")
)足以防止 GC。
在 System.Threading.Timer
上使用单参数构造函数也有效,因为计时器随后会将自身作为 state
参数引用。以下代码在 GC 之后使两个计时器保持活动状态,因为它们都被 GCHandle
表中的回调引用:
class Program
{
static void Main(string[] args)
{
System.Threading.Timer timer1 = null;
timer1 = new System.Threading.Timer(_ => Console.WriteLine("Stayin alive (1)..."));
timer1.Change(0, 400);
var timer2 = new System.Timers.Timer
{
Interval = 400,
AutoReset = true
};
timer2.Elapsed += (_, __) => Console.WriteLine("Stayin alive (2)...");
timer2.Enabled = true;
System.Threading.Thread.Sleep(2000);
Console.WriteLine("Invoking GC.Collect...");
GC.Collect();
Console.ReadKey();
}
}
timer1
甚至被垃圾收集?不是还在范围内吗?
您可以使用 windbg、sos 和 !gcroot
回答此问题和类似问题
0:008> !gcroot -nostacks 0000000002354160
DOMAIN(00000000002FE6A0):HANDLE(Strong):241320:Root:00000000023541a8(System.Thre
ading._TimerCallback)->
00000000023540c8(System.Threading.TimerCallback)->
0000000002354050(System.Timers.Timer)->
0000000002354160(System.Threading.Timer)
0:008>
在这两种情况下,本机计时器都必须阻止回调对象的 GC(通过 GCHandle)。不同之处在于,在 System.Timers.Timer
的情况下,回调引用 System.Timers.Timer
对象(在内部使用 System.Threading.Timer
实现)
在查看了 Task.Delay 的一些示例实现并做了一些实验之后,我最近一直在谷歌搜索这个问题。
原来 System.Threading.Timer 是不是 GCd 就看你怎么构造了!!!
如果仅使用回调构造,则状态对象将是计时器本身,这将防止它被 GC'd。这似乎在任何地方都没有记录,但是没有它,就很难创建火灾和忘记计时器。
此代码中的注释还说明了为什么如果回调引用 new 返回的计时器对象,则使用仅回调 ctor 总是更好的原因,否则可能会出现竞争错误。
在 timer1 中,您正在给它一个回调。在 timer2 中,您正在连接一个事件处理程序;这设置了对您的 Program 类的引用,这意味着计时器不会被 GCed。由于您不再使用 timer1 的值,(基本上与删除 var timer1 = 相同)编译器足够聪明,可以优化掉变量。当你点击 GC 调用时,没有任何东西引用 timer1,所以它被收集了。
在您的 GC 调用之后添加一个 Console.Writeline 以输出 timer1 的属性之一,您会注意到它不再被收集。
Program
类的引用,即使有,也不会阻止计时器被 GC'ed。
仅供参考,从 .NET 4.6(如果不是更早的版本)开始,这似乎不再正确。您的测试程序在今天运行时不会导致任何一个计时器被垃圾收集。
Stayin alive (1)...
Stayin alive (2)...
Stayin alive (1)...
Stayin alive (2)...
Stayin alive (1)...
Stayin alive (2)...
Stayin alive (1)...
Stayin alive (2)...
Stayin alive (1)...
Invoking GC.Collect...
Stayin alive (2)...
Stayin alive (1)...
Stayin alive (2)...
Stayin alive (1)...
Stayin alive (2)...
Stayin alive (1)...
Stayin alive (2)...
Stayin alive (1)...
Stayin alive (2)...
Stayin alive (1)...
Stayin alive (2)...
Stayin alive (1)...
当我查看 implementation of System.Threading.Timer 时,这似乎是有道理的,因为当前版本的 .NET 使用活动计时器对象的链表,并且该链表由 TimerQueue 内的成员变量保存(这是一个单例对象也由 TimerQueue 中的静态成员变量保持活动状态)。因此,只要所有计时器实例处于活动状态,它们就会保持活动状态。
System.Threading.Timer
实例。请确保您在发布模式下编译代码并启用优化。您提到的链表包含辅助 TimerQueueTimer
对象;它不会阻止 GC 收集原始 System.Threading.Timer
实例。 (每个 System.Threading.Timer
实例引用其自己的 TimerQueueTimer
对象,但反之则不然。当 GC 收集 System.Threading.Timer
时,~TimerHolder
终结器将其 TimerQueueTimer
对象从队列中删除。)
Main
方法之外。
Remarks
部分 只要您使用 Timer,就必须保留对它的引用。与任何托管对象一样,当没有对 Timer 的引用时,它会受到垃圾回收的影响。 Timer 仍然处于活动状态这一事实并不能阻止它被收集。 在这种情况下,静态TimerQueue
保留对计时器实例的引用,因此不会被收集。