为什么System.Timers.Timer存活GC,但不是System.Threading.Timer?

看来, 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,在debugging器外面)时,只有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表中注册callback( System.Threading._TimerCallback )。 正如汉斯在他的评论中指出的那样,当这个完成时, state参数也保持活跃。

正如John指出的那样, System.Timers.Timer保持活动的原因是因为它被callback(它作为stateparameter passing给内部System.Threading.Timer )引用。 同样,我们的System.Threading.Timer是GC'ed的原因是因为它没有被它的callback引用。

添加一个明确的引用到timer1的callback(例如, Console.WriteLine("Stayin alive (" + timer1.GetType().FullName + ")") )就足以防止GC。

System.Threading.Timer上使用单参数构造函数也是可行的,因为定时器会引用自己作为state参数。 下面的代码使两个定时器在GC之后保持活动状态,因为它们分别由GCHandle表的callback引用:

 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(); } } 

你可以用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> 

在这两种情况下,本地计时器必须阻止callback对象的GC(通过GCHandle)。 不同之处在于,在System.Timers.Timer的情况下,callback引用了System.Timers.Timer对象(这是使用System.Threading.Timer内部实现的)

在查看Task.Delay的一些示例实现并做了一些实验之后,我一直在使用这个问题。

事实certificate,System.Threading.Timer是否GCd取决于你如何构造它!

如果只用一个callback来构造,那么状态对象就是计时器本身,这将阻止它被GC_d。 这似乎没有logging在任何地方,但没有它,这是非常困难的创造火灾,忘记定时器。

我从http://www.dotnetframework.org/default.aspx/DotNET/DotNET/8@0/untmp/whidbey/REDBITS/ndp/clr/src/BCL/System/Threading/Timer@cs / 1 /定时器@ CS

这段代码中的注释也说明了为什么总是使用callback函数,如果callback引用了new返回的定时器对象,那么总是会更好,否则可能会有一个比赛错误。

在timer1中,你给它一个callback。 在timer2中,你正在连接一个事件处理程序; 这设置了一个对你的程序类的引用,这意味着定时器不会被GCed。 既然你永远不会再使用timer1的值,(就好像你删除了var timer1 =一样),编译器足够聪明地优化掉variables。 当你打了GC呼叫,没有什么是引用timer1了,所以它的'收集。

在GC调用之后添加一个Console.Writeline来输出timer1的一个属性,你会发现它不再被收集。

仅供参考,.NET 4.6(如果不是更早),这似乎不再是真实的。 您的testing程序在今天运行时,不会导致任何计时器被垃圾收集。

 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)... 

当我看着System.Threading.Timer的实现时 ,这似乎是有道理的,因为看起来当前版本的.NET使用活动计时器对象的链接列表,并且该链接列表由TimerQueue中的成员variables保存一个单独的对象也由TimerQueue中的静态成员variables保持)。 因此,只要活动,所有计时器实例都将保持活动状态。

您可以使用

 GC.KeepAlive(timer1); 

防止垃圾收集在这个对象上。