为啥 System.Timers.Timer 能在 GC 中存活,而 System.Threading.Timer 不能?
Posted
技术标签:
【中文标题】为啥 System.Timers.Timer 能在 GC 中存活,而 System.Threading.Timer 不能?【英文标题】:Why does a System.Timers.Timer survive GC but not System.Threading.Timer?为什么 System.Timers.Timer 能在 GC 中存活,而 System.Threading.Timer 不能? 【发布时间】:2011-06-25 03:11:25 【问题描述】: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.***Exception
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
正如约翰在回答中指出的那样,两个计时器都在 GCHandle
表中注册了它们的回调 (System.Threading._TimerCallback
)。正如 Hans 在他的评论中指出的那样,state
参数在完成后也会保持活动状态。
正如约翰所指出的,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
甚至被垃圾收集?不是还在范围内吗?
Jeff:范围并不重要。这几乎就是 GC.KeepAlive 方法存在的理由。如果您对挑剔的细节感兴趣,请参阅blogs.msdn.com/b/cbrumme/archive/2003/04/19/51365.aspx。
在 Timer.Enabled 设置器中查看 Reflector。请注意它与“cookie”一起使用的技巧,它为系统计时器提供了一个状态对象以在回调中使用。 CLR 知道它, clr/src/vm/comthreadpool.cpp, CorCreateTimer() 在 SSCLI20 源代码中。 MakeDelegateInfo() 变得复杂。
@Jeff:这是预期的行为; CLR via C# 中的详细信息并在“当 JIT 编译器行为不同时(调试)”下提到了 on my blog。
@StephenCleary 哇 - 精神。我刚刚在一个围绕 System.Timers.Timer 的应用程序中发现了一个错误,该错误在我预计它会死掉之后保持活跃并发布更新。感谢您节省了很多时间!
【参考方案1】:
您可以使用 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
实现)
【讨论】:
【参考方案2】:在查看了 Task.Delay 的一些示例实现并做了一些实验之后,我最近一直在谷歌上搜索这个问题。
原来System.Threading.Timer是不是GCd就看你怎么构造了!!!
如果仅使用回调构造,则状态对象将是计时器本身,这将防止它被 GC'd。这似乎在任何地方都没有记录,但没有它,就很难创建“一劳永逸”的计时器。
我从http://www.dotnetframework.org/default.aspx/DotNET/DotNET/8@0/untmp/whidbey/REDBITS/ndp/clr/src/BCL/System/Threading/Timer@cs/1/Timer@cs的代码中找到了这个
此代码中的 cmets 还说明了为什么如果回调引用 new 返回的计时器对象总是更好地使用仅回调 ctor,否则可能会出现竞争错误。
【讨论】:
它实际上符合文档。如果您在回调中保留对 System.Threading.Timer 的引用(通过闭包或构造函数),那么它不会像任何托管对象一样被收集。 @joe 您在文档中有参考吗,因为我似乎在 Timer 或 TimerCallback 主页面上找不到提到的内容? @user3797758 docs 没有直接提到它,但它与这个anwser对齐。请参阅Remarks
部分只要您使用 Timer,就必须保留对它的引用。与任何托管对象一样,当没有对 Timer 的引用时,它会受到垃圾回收的影响。 Timer 仍然处于活动状态这一事实并不能阻止它被收集。 在这种情况下,静态 TimerQueue
保留对计时器实例的引用,因此不会被收集。
是的,我也读过那部分,但这与任何其他 C# 对象没有什么不同,我以为你的评论有别的意思......【参考方案3】:
在 timer1 中,您正在给它一个回调。在 timer2 中,您正在连接一个事件处理程序;这设置了对您的 Program 类的引用,这意味着计时器不会被 GCed。由于您再也不会使用 timer1 的值,(基本上与删除 var timer1 = 相同)编译器足够聪明,可以优化掉变量。当你点击 GC 调用时,没有任何东西引用 timer1,所以它被收集了。
在您的 GC 调用之后添加一个 Console.Writeline 以输出 timer1 的属性之一,您会注意到它不再被收集。
【讨论】:
事件处理程序没有对Program
类的引用,即使有,也不会阻止定时器被GC。
是的。编译上面的代码,然后用.Net反射器查看。 += lamba 被转换为 Program 类中的方法。是的,被链接的事件处理程序确实会阻止垃圾收集。 blogs.msdn.com/b/abhinaba/archive/2009/05/05/…【参考方案4】:
仅供参考,从 .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
实例被收集在 .NET 4.6 中。请确保您在发布模式下编译代码并启用优化。您提到的链表包含帮助器TimerQueueTimer
对象;它不会阻止原始 System.Threading.Timer
实例被 GC 收集。 (每个System.Threading.Timer
实例引用它自己的TimerQueueTimer
对象,反之则不然。当System.Threading.Timer
被GC 收集时,它的TimerQueueTimer
对象被~TimerHolder
终结器从队列中移除。)跨度>
在发布模式下试一试。
这里相同,在 .NET Core 3.0 上运行代码。在 Debug 或 Release 构建中,这两个计时器都没有被垃圾收集。要重现该问题,我必须将计时器的创建移到 Main
方法之外。以上是关于为啥 System.Timers.Timer 能在 GC 中存活,而 System.Threading.Timer 不能?的主要内容,如果未能解决你的问题,请参考以下文章
为啥 System.Timers.Timer 在触发 Elapsed 事件时会创建多个线程?
System.Timers.Timer 与 System.Threading.Timer
使用System.Timers.Timer类实现程序定时执行
System.Windows.Forms.TimerSystem.Timers.TimerSystem.Threading.Timer