为什么System.Timers.Timer能够在GC中存活但不能在System.Threading.Timer中存活?

时间:2011-02-10 20:18:49

标签: .net timer garbage-collection

似乎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的:

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

正如约翰在答案中指出的那样,两个计时器都在System.Threading._TimerCallback表中注册了他们的回调(GCHandle)。汉斯在评论中指出,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();
  }
}

4 个答案:

答案 0 :(得分:31)

您可以使用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在内部实现)

答案 1 :(得分:8)

在查看Task.Delay的一些示例实现并进行一些实验后,我最近一直在谷歌上搜索这个问题。

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

如果仅使用回调构造,则状态对象将是计时器本身,这将阻止它进行GC。这似乎没有在任何地方记录,但没有它,创建火灾和忘记计时器是非常困难的。

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

的代码中找到的

此代码中的注释还指出,如果回调引用new返回的计时器对象,则使用仅回调ctor总是更好,否则可能存在种族错误。

答案 2 :(得分:1)

在timer1中,你给它一个回调。在timer2中,您正在连接一个事件处理程序;这会设置对Program类的引用,这意味着计时器不会被GCed。由于您再也不使用timer1的值(基本上与删除var timer1 =相同),编译器足够智能以优化变量。当你点击GC调用时,没有任何内容正在引用timer1,因此它被“收集”。

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

答案 3 :(得分: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)...

当我查看implementation of System.Threading.Timer时,这似乎是有道理的,因为当前版本的.NET似乎使用活动计时器对象的链接列表,并且链接列表由{{3内的成员变量保存(也是在TimerQueue中由静态成员变量保持活动的单例对象)。因此,只要它们处于活动状态,所有计时器实例都将保持活动状态。