为什么一个System.Timers.Timer的生存GC但不System.Threading.Timer?但不、Timers、System、Timer

2023-09-02 01:25:10 作者:╰★我为王者ヴ

看来, System.Timers.Timer的情况下保持活力的一些机制,但 System.Threading.Timer 实例都没有。

示例程序,用一个周期性的 System.Threading.Timer 和自动复位 System.Timers.Timer的

 类节目
{
  静态无效的主要(字串[] args)
  {
    VAR定时器1 =新System.Threading.Timer(
      _ => Console.WriteLine(留驻活(1)...),
      空值,
      0,
      400);

    VAR定时器2 =新System.Timers.Timer的
    {
      间隔= 400,
      自动复位=真
    };
    timer2.Elapsed + =(_,_)=> Console.WriteLine(留驻活(2)...);
    timer2.Enabled = TRUE;

    System.Threading.Thread.Sleep(2000);

    Console.WriteLine(调用GC.Collect的......);
    所以GC.Collect();

    Console.ReadKey();
  }
}
 

当我运行这个程序(.NET 4.0客户端,发布,外调试器),只有 System.Threading.Timer 是GC'ed:

 留驻活着(1)...
留驻活着(1)...
留驻活着(2)...
留驻活着(1)...
留驻活着(2)...
留驻活着(1)...
留驻活着(2)...
留驻活着(1)...
留驻活着(2)...
调用GC.Collect的...
留驻活着(2)...
留驻活着(2)...
留驻活着(2)...
留驻活着(2)...
留驻活着(2)...
留驻活着(2)...
留驻活着(2)...
留驻活着(2)...
留驻活着(2)...
 

修改:我已经接受低于约翰的回答,但我想它阐述了一下

为什么 System.Timers.Timer 更改间隔时间后的第一次触发时间是设定时间的三倍 ...

在(在睡眠与断点)运行样本程序上面,这里所讨论的对象的状态和的GCHandle 表:

 !DSO
操作系统线程ID:0x838(2104)
ESP / REG对象名称
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
域(0015DC38):手柄(强):9911c0:根:00c2c05c(System.Threading._TimerCallback) - >
  00c2bfe8(System.Threading.TimerCallback) - >
  00c2bfb0(System.Timers.Timer的) - >
  00c2c034(System.Threading.Timer)

!gchandles
GC手柄统计:
强手柄:22
固定把手:5
异步已固定把手:0
引用计数手:0
弱龙手柄:0
弱短手柄:0
其他句柄:0
统计:
      MT计数TotalSize类名称
7aa132b4 1 12 System.Diagnostics.TraceListenerCollection
79b9f720 1 12 System.Object的
79ba1c50 1月28日System.SharedStatics
79ba37a8 1 36 System.Security.PermissionSet
79baa9​​40 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的[]
共27个对象
 

正如约翰指出,在他的回答,既计时器记录他们的回调( System.Threading._TimerCallback )的的GCHandle 表。正如汉斯指出,在他的评论中,状态参数也保持活着的时候这样做。

正如约翰指出,其原因 System.Timers.Timer的保持活着是因为它是由回调(它是通过为状态参数内 System.Threading.Timer );同样,我们之所以 System.Threading.Timer 是GC'ed是因为它的没有的其回调引用。

添加一个明确提及定时器的回调(例如, Console.WriteLine(留驻活(+ timer1.GetType()。全名+)))足以prevent GC。

System.Threading.Timer 使用单参数的构造函数也适用,因为计时器将随即引用自身的状态参数。下面code保留两个定时器活着GC之后,因为它们是每个引用从的GCHandle 表的回调:

 类节目
{
  静态无效的主要(字串[] args)
  {
    System.Threading.Timer定时器1 = NULL;
    定时器1 =新System.Threading.Timer(_ => Console.WriteLine(留驻活(1)...));
    timer1.Change(0,400);

    VAR定时器2 =新System.Timers.Timer的
    {
      间隔= 400,
      自动复位=真
    };
    timer2.Elapsed + =(_,_)=> Console.WriteLine(留驻活(2)...);
    timer2.Enabled = TRUE;

    System.Threading.Thread.Sleep(2000);

    Console.WriteLine(调用GC.Collect的......);
    所以GC.Collect();

    Console.ReadKey();
  }
}
 

解决方案

您可以回答这个问题和类似的问题与WinDbg中,SOS儿童村,和!gcroot

  0:008> !gcroot -nostacks 0000000002354160
域(00000000002FE6A0):手柄(强):241320:根:00000000023541a8(System.Thre
ading._TimerCallback) - >
00000000023540c8(System.Threading.TimerCallback) - >
0000000002354050(System.Timers.Timer的) - >
0000000002354160(System.Threading.Timer)
0:008>
 

在这两种情况下,本机定时器具有回调对象的prevent GC(经由的GCHandle)。所不同的是,在的情况下, System.Timers.Timer的回调引用 System.Timers.Timer的对象(这是内部实施使用 System.Threading.Timer

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

Sample program, with a periodic System.Threading.Timer and auto-reset 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:

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:

!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

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.

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.

Adding an explicit reference to timer1's callback (e.g., Console.WriteLine("Stayin alive (" + timer1.GetType().FullName + ")")) is sufficient to prevent 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:

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

解决方案

You can answer this and similar questions with windbg, sos, and !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)