ChatGPT解决这个技术问题 Extra ChatGPT

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

似乎 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.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 在回答中指出的那样,两个计时器都在 GCHandle 表中注册了它们的回调 (System.Threading._TimerCallback)。正如 Hans 在他的评论中指出的那样,完成此操作后,state 参数也会保持活动状态。

正如 John 所指出的,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() 变得复杂。
@StephenCleary 哇——精神上的。我刚刚在一个围绕 System.Timers.Timer 的应用程序中发现了一个错误,该错误在我预计它会死掉之后保持活跃并发布更新。感谢您节省了很多时间!
所以,如果你总是在构造函数之后自己调用 timer1.Change(dueTime, period),你不会被 GC 吓到。

J
John

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


N
Nick H

在查看了 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 的代码中找到了这个

此代码中的注释还说明了为什么如果回调引用 new 返回的计时器对象,则使用仅回调 ctor 总是更好的原因,否则可能会出现竞争错误。


它实际上符合文档。如果您在回调中保留对 System.Threading.Timer 的引用(通过闭包或构造函数),则它不会像任何托管对象一样被收集。
@joe您在文档中有参考吗,因为我似乎在 Timer 或 TimerCallback 主页面上找不到提到的内容?
@user3797758 docs 没有直接提及它,但它与此 anwser 保持一致。请参阅 Remarks 部分 只要您使用 Timer,就必须保留对它的引用。与任何托管对象一样,当没有对 Timer 的引用时,它会受到垃圾回收的影响。 Timer 仍然处于活动状态这一事实并不能阻止它被收集。 在这种情况下,静态 TimerQueue 保留对计时器实例的引用,因此不会被收集。
是的,我也读过那部分,但这与任何其他 C# 对象没有什么不同,我认为你的评论意味着其他东西......
A
Andy

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

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


事件处理程序没有对 Program 类的引用,即使有,也不会阻止计时器被 GC'ed。
是的。编译上面的代码,然后用.Net reflector 看看。 += lamba 被转换为 Program 类中的方法。是的,被链接的事件处理程序确实会阻止垃圾收集。 blogs.msdn.com/b/abhinaba/archive/2009/05/05/…
E
Erv Walter

仅供参考,从 .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 中的静态成员变量保持活动状态)。因此,只要所有计时器实例处于活动状态,它们就会保持活动状态。


我仍然看到在 .NET 4.6 中收集了 System.Threading.Timer 实例。请确保您在发布模式下编译代码并启用优化。您提到的链表包含辅助 TimerQueueTimer 对象;它不会阻止 GC 收集原始 System.Threading.Timer 实例。 (每个 System.Threading.Timer 实例引用其自己的 TimerQueueTimer 对象,但反之则不然。当 GC 收集 System.Threading.Timer 时,~TimerHolder 终结器将其 TimerQueueTimer 对象从队列中删除。)
在发布模式下尝试。
同样在这里,在 .NET Core 3.0 上运行代码。在 Debug 或 Release 构建中,这两个计时器都没有被垃圾收集。要重现该问题,我必须将计时器的创建移到 Main 方法之外。