为什么一个System.Timers。计时器在GC中存活,而不是system . thread .Timer?

时间:2022-07-21 20:38:11

It appears that System.Timers.Timer instances are kept alive by some mechanism, but System.Threading.Timer instances are not.

看来System.Timers。计时器实例通过某种机制而不是System.Threading来保持活动状态。计时器实例。

Sample program, with a periodic System.Threading.Timer and auto-reset System.Timers.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();
  }
}

When I run this program (.NET 4.0 Client, Release, outside the debugger), only the System.Threading.Timer is GC'ed:

当我运行这个程序时。NET 4.0客户端,释放,除调试器外),只有System.Threading。“计时器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)...

EDIT: I've accepted John's answer below, but I wanted to expound on it a bit.

编辑:我已经接受了下面约翰的回答,但是我想对它进行一些阐述。

When running the sample program above (with a breakpoint at Sleep), here's the state of the objects in question and the GCHandle table:

当运行上面的示例程序(在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

As John pointed out in his answer, both timers register their callback (System.Threading._TimerCallback) in the GCHandle table. As Hans pointed out in his comment, the state parameter is also kept alive when this is done.

正如John在回答中指出的,两个计时器都在GCHandle表中注册它们的回调(system . thread . _timercallback)。正如汉斯在他的评论中指出的那样,在完成该操作时,状态参数也将保持活动状态。

As John pointed out, the reason System.Timers.Timer is kept alive is because it is referenced by the callback (it is passed as the state parameter to the inner System.Threading.Timer); likewise, the reason our System.Threading.Timer is GC'ed is because it is not referenced by its callback.

正如约翰指出的,原因系统。定时器。Timer之所以保持活动是因为它被回调引用(它作为状态参数传递给内部system . thread .Timer);同样,我们的系统。线程。Timer是GC - ed,是因为它的回调没有引用它。

Adding an explicit reference to timer1's callback (e.g., Console.WriteLine("Stayin alive (" + timer1.GetType().FullName + ")")) is sufficient to prevent GC.

向timer1的回调添加显式引用(例如,控制台)。WriteLine("Stayin alive " + timer1.GetType())。)足以防止GC。

Using the single-parameter constructor on System.Threading.Timer also works, because the timer will then reference itself as the state parameter. The following code keeps both timers alive after the GC, since they are each referenced by their callback from the GCHandle table:

在System.Threading中使用单参数构造函数。计时器也可以工作,因为计时器会将自己引用为状态参数。下面的代码使这两个计时器在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 个解决方案

#1


28  

You can answer this and similar questions with windbg, sos, and !gcroot

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

In both cases, the native timer has to prevent GC of the callback object (via a GCHandle). The difference is that in the case of System.Timers.Timer the callback references the System.Timers.Timer object (which is implemented internally using a System.Threading.Timer)

在这两种情况下,本机计时器都必须防止回调对象的GC(通过GCHandle)。不同之处在于system . timer的情况。定时器,回调引用系统。定时器。定时器对象(使用system . thread .Timer在内部实现)

#2


6  

I have been googling this issue recently after looking at some example implementations of Task.Delay and doing some experiments.

最近,在查看了Task的一些实现示例之后,我在谷歌上搜索了这个问题。延迟和做一些实验。

It turns out that whether or not System.Threading.Timer is GCd depends on how you construct it!!!

结果是System.Threading。定时器是GCd取决于你如何构建它!!

If constructed with just a callback then the state object will be the timer itself and this will prevent it from being GC'd. This does not appear to be documented anywhere and yet without it it is extremely difficult to create fire and forget timers.

如果仅用一个回调构造,那么状态对象将是计时器本身,这将阻止它成为GC。这似乎并没有被记录在任何地方,但没有它,创建火灾和忘记定时器是极其困难的。

I found this from the code at http://www.dotnetframework.org/default.aspx/DotNET/DotNET/8@0/untmp/whidbey/REDBITS/ndp/clr/src/BCL/System/Threading/Timer@cs/1/Timer@cs

我在http://www.dotnetframework.org/default.aspx/dotnet/dotnet/8@ntmp /untmp/whidbey/REDBITS/ndp/clr/src/BCL/System/Threading/Timer@cs/1/ timer@ter@cs.cs中发现了这一点

The comments in this code also indicate why it is always better to use the callback-only ctor if the callback references the timer object returned by new as otherwise there could be a race bug.

这段代码中的注释还指出,如果回调引用new返回的timer对象(否则可能会出现竞争错误),那么使用只回调的ctor总是更好的。

#3


0  

In timer1 you're giving it a callback. In timer2 to you're hooking up an event handler; this setups up a reference to your Program class which means the timer won't be GCed. Since you never use the value of timer1 again, (basically the same as if you removed the var timer1 = ) the compiler is smart enough to optimize away the variable. When you hit the GC call, nothing is referencing timer1 anymore so its' collected.

在timer1中,你给它一个回调。在timer2中,你需要连接一个事件处理器;这将建立对程序类的引用,这意味着计时器不会被GCed。由于不再使用timer1的值(基本上与删除var timer1 =相同),因此编译器非常聪明,可以对变量进行优化。当您点击GC调用时,没有任何东西再引用timer1,所以它是‘collect’。

Add a Console.Writeline after your GC call to output one of the properties of timer1 and you'll notice it's not collected anymore.

添加一个控制台。在GC调用后写入,以输出timer1的一个属性,您将注意到它不再被收集。

#4


0  

FYI, as of .NET 4.6 (if not earlier), this appears to not be true anymore. Your test program, when run today, does not result in either timer being garbage collected.

参考一下。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)...

As I look at the implementation of System.Threading.Timer, this seems to make sense as it appears that the current version of .NET uses linked list of active timer objects and that linked list is held by a member variable inside TimerQueue (which is a singleton object kept alive by a static member variable also in TimerQueue). As a result, all timer instances will be kept alive as long as they are active.

当我看到System.Threading的实现时。Timer,这似乎是有意义的,因为当前版本的。net使用了活动计时器对象的链表,而链表由TimerQueue中的一个成员变量持有(TimerQueue中也有一个静态成员变量)。因此,只要所有的计时器实例是活动的,它们就会一直保持活动状态。

#1


28  

You can answer this and similar questions with windbg, sos, and !gcroot

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

In both cases, the native timer has to prevent GC of the callback object (via a GCHandle). The difference is that in the case of System.Timers.Timer the callback references the System.Timers.Timer object (which is implemented internally using a System.Threading.Timer)

在这两种情况下,本机计时器都必须防止回调对象的GC(通过GCHandle)。不同之处在于system . timer的情况。定时器,回调引用系统。定时器。定时器对象(使用system . thread .Timer在内部实现)

#2


6  

I have been googling this issue recently after looking at some example implementations of Task.Delay and doing some experiments.

最近,在查看了Task的一些实现示例之后,我在谷歌上搜索了这个问题。延迟和做一些实验。

It turns out that whether or not System.Threading.Timer is GCd depends on how you construct it!!!

结果是System.Threading。定时器是GCd取决于你如何构建它!!

If constructed with just a callback then the state object will be the timer itself and this will prevent it from being GC'd. This does not appear to be documented anywhere and yet without it it is extremely difficult to create fire and forget timers.

如果仅用一个回调构造,那么状态对象将是计时器本身,这将阻止它成为GC。这似乎并没有被记录在任何地方,但没有它,创建火灾和忘记定时器是极其困难的。

I found this from the code at http://www.dotnetframework.org/default.aspx/DotNET/DotNET/8@0/untmp/whidbey/REDBITS/ndp/clr/src/BCL/System/Threading/Timer@cs/1/Timer@cs

我在http://www.dotnetframework.org/default.aspx/dotnet/dotnet/8@ntmp /untmp/whidbey/REDBITS/ndp/clr/src/BCL/System/Threading/Timer@cs/1/ timer@ter@cs.cs中发现了这一点

The comments in this code also indicate why it is always better to use the callback-only ctor if the callback references the timer object returned by new as otherwise there could be a race bug.

这段代码中的注释还指出,如果回调引用new返回的timer对象(否则可能会出现竞争错误),那么使用只回调的ctor总是更好的。

#3


0  

In timer1 you're giving it a callback. In timer2 to you're hooking up an event handler; this setups up a reference to your Program class which means the timer won't be GCed. Since you never use the value of timer1 again, (basically the same as if you removed the var timer1 = ) the compiler is smart enough to optimize away the variable. When you hit the GC call, nothing is referencing timer1 anymore so its' collected.

在timer1中,你给它一个回调。在timer2中,你需要连接一个事件处理器;这将建立对程序类的引用,这意味着计时器不会被GCed。由于不再使用timer1的值(基本上与删除var timer1 =相同),因此编译器非常聪明,可以对变量进行优化。当您点击GC调用时,没有任何东西再引用timer1,所以它是‘collect’。

Add a Console.Writeline after your GC call to output one of the properties of timer1 and you'll notice it's not collected anymore.

添加一个控制台。在GC调用后写入,以输出timer1的一个属性,您将注意到它不再被收集。

#4


0  

FYI, as of .NET 4.6 (if not earlier), this appears to not be true anymore. Your test program, when run today, does not result in either timer being garbage collected.

参考一下。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)...

As I look at the implementation of System.Threading.Timer, this seems to make sense as it appears that the current version of .NET uses linked list of active timer objects and that linked list is held by a member variable inside TimerQueue (which is a singleton object kept alive by a static member variable also in TimerQueue). As a result, all timer instances will be kept alive as long as they are active.

当我看到System.Threading的实现时。Timer,这似乎是有意义的,因为当前版本的。net使用了活动计时器对象的链表,而链表由TimerQueue中的一个成员变量持有(TimerQueue中也有一个静态成员变量)。因此,只要所有的计时器实例是活动的,它们就会一直保持活动状态。