为什么System.Timers.Timer在GC中幸存,但在System.Threading.Timer中却不行?


76

看来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.TimerGC进行了:

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在其答案中指出的那样,两个计时器都System.Threading._TimerCallbackGCHandle表中注册其回调()。正如汉斯在他的评论中指出的那样,该state参数在完成后也保持有效。

正如John所指出的,之所以System.Timers.Timer保持活动是因为它被回调引用(它作为state参数传递给inner System.Threading.Timer)。同样,我们使用System.Threading.TimerGC的原因是因为其回调引用它。

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

1
为什么timer1还要进行垃圾收集?还在范围内吗?
杰夫·斯特恩

3
杰夫:范围并不十分相关。这几乎是GC.KeepAlive方法的存在理由。如果您对挑剔的细节感兴趣,请参阅blogs.msdn.com/b/cbrumme/archive/2003/04/19/51365.aspx
妮可·卡利诺乌

7
看看Timer.Enabled设置器上的Reflector。请注意,它与“ cookie”一起使用的技巧为系统计时器提供了要在回调中使用的状态对象。CLR在SSCLI20源代码中的clr / src / vm / comthreadpool.cpp,CorCreateTimer()中就知道了。MakeDelegateInfo()变得很复杂。
汉斯·帕桑

1
@Jeff:这是预期的行为;通过C#进行CLR中的详细信息,并在我的博客的“何时JIT编译器行为不同(调试)”下提到。
Stephen Cleary

2
@StephenCleary哇-精神病。我只是在一个围绕System.Timers.Timer的应用程序中发现了一个错误,在我期望它死掉之后,Timer仍然有效并发布更新。感谢您节省大量时间!
Andrew Burnett-Thompson博士

Answers:


33

您可以使用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


11

在查看Task.Delay的一些示例实现并进行了一些实验之后,我最近一直在研究这个问题。

事实证明System.Threading.Timer是否为GCd取决于您如何构造它!

如果仅使用回调构造,则状态对象将是计时器本身,这将防止其被GC回收。这似乎没有记录在任何地方,但是如果没有它,则极有可能造成火灾并忘记计时器。

我是从http://www.dotnetframework.org/default.aspx/DotNET/DotNET/8@0/untmp/whidbey/REDBITS/ndp/clr/src/Brc/System/Threading/Timer@cs的代码中找到的/ 1 / Timer @ cs

这段代码中的注释还表明,如果回调引用了new返回的计时器对象,那么为什么总是使用仅回调的ctor总是更好的选择,否则可能会出现竞速错误。


它实际上符合文档。如果在回调中保留对System.Threading.Timer的引用(通过闭包或构造函数),则不会像任何托管对象一样收集它。

1

在timer1中,您为其提供了回调。在timer2中,您正在连接一个事件处理程序;这将建立对您的Program类的引用,这意味着不会对计时器进行GC处理。由于不再使用timer1的值了(基本上与删除var timer1 =相同),因此编译器足够聪明,可以优化该变量。当您打GC呼叫时,不再引用Timer1,因此已收集它。

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


3
事件处理程序没有对该Program类的引用,即使引用了该类,也不会阻止对计时器进行GC处理。
Stephen Cleary

3
是的。编译上面的代码,然后使用.Net反射器查看它。+ =兰巴转换为Program类中的方法。是的,链接的事件处理程序确实可以防止垃圾回收。 blogs.msdn.com/b/abhinaba/archive/2009/05/05/…–
Andy

0

仅供参考,从.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)...

当我查看System.Threading.Timer实现时,这似乎很有意义,因为当前的.NET版本使用的是活动计时器对象的链接列表,而该链接列表由TimerQueue中的成员变量(即同样由TimerQueue中的静态成员变量保持活动的单例对象)。结果,所有计时器实例只要处于活动状态都将保持活动状态。


2
我仍然看到System.Threading.Timer实例是在.NET 4.6中收集的。请确保您在启用优化的发布模式下编译代码。您提到的链表包含帮助器TimerQueueTimer对象。它不会阻止System.Threading.TimerGC收集原始实例。(每个System.Threading.Timer实例都引用自己的TimerQueueTimer对象,反之亦然System.Threading.Timer。GC收集时,终结器TimerQueueTimer将从队列中删除其对象~TimerHolder。)
Antosha

在发布模式下尝试。
Ievgen Naida

同样,在.NET Core 3.0上运行代码。在Debug或Release版本中,两个计时器都不会被垃圾回收。要重现该问题,我必须将计时器的创建移到该Main方法之外。
Theodor Zoulias
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.