Warum überlebt ein System.Timers.Timer die GC, nicht jedoch System.Threading.Timer?

75

Es scheint, dass System.Timers.TimerInstanzen durch einen Mechanismus am Leben erhalten werden, System.Threading.TimerInstanzen jedoch nicht.

Beispielprogramm mit periodischem System.Threading.Timerund automatischem Zurücksetzen 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();
  }
}

Wenn ich dieses Programm ausführe (.NET 4.0 Client, Release, außerhalb des Debuggers), wird nur das System.Threading.TimerGC'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 : Ich habe Johns Antwort unten akzeptiert, aber ich wollte es ein wenig erläutern.

Wenn Sie das obige Beispielprogramm ausführen (mit einem Haltepunkt bei Sleep), sehen Sie hier den Status der betreffenden Objekte und die GCHandleTabelle:

!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

Wie John in seiner Antwort betonte, registrieren beide Timer ihren Rückruf ( System.Threading._TimerCallback) in der GCHandleTabelle. Wie Hans in seinem Kommentar betonte, statebleibt der Parameter auch dann am Leben.

Wie John betonte, System.Timers.Timerbleibt der Grund am Leben, weil er vom Rückruf referenziert wird (er wird als stateParameter an das Innere übergeben System.Threading.Timer); Ebenso ist der Grund, warum wir System.Threading.TimerGC'ed sind, der, dass es nicht durch seinen Rückruf referenziert wird.

Das Hinzufügen eines expliziten Verweises auf timer1den Rückruf (z. B. Console.WriteLine("Stayin alive (" + timer1.GetType().FullName + ")")) reicht aus, um GC zu verhindern.

Die Verwendung des Einzelparameter-Konstruktors System.Threading.Timerfunktioniert auch, da sich der Timer dann selbst als stateParameter referenziert . Der folgende Code hält beide Timer nach dem GC am Leben, da sie jeweils durch ihren Rückruf aus der GCHandleTabelle referenziert werden :

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();
  }
}
Stephen Cleary
quelle
1
Warum wird timer1überhaupt Müll gesammelt? Ist es nicht noch im Geltungsbereich?
Jeff Sternal
3
Jeff: Der Umfang ist nicht wirklich relevant. Dies ist so ziemlich die Daseinsberechtigung für die GC.KeepAlive-Methode. Wenn Sie an den wählerischen Details interessiert sind, lesen Sie blogs.msdn.com/b/cbrumme/archive/2003/04/19/51365.aspx .
Nicole Calinoiu
7
Schauen Sie sich mit Reflector den Timer an. Aktivierter Setter. Beachten Sie den Trick, der mit "Cookie" verwendet wird, um dem Systemzeitgeber ein Statusobjekt zu geben, das im Rückruf verwendet werden soll. Der CLR ist dies bekannt: clr / src / vm / comthreadpool.cpp, CorCreateTimer () im SSCLI20-Quellcode. MakeDelegateInfo () wird kompliziert.
Hans Passant
1
@ Jeff: Das ist erwartetes Verhalten; Details in CLR über C # und in meinem Blog unter "Wenn sich der JIT-Compiler anders verhält (Debug)" erwähnt.
Stephen Cleary
1
@ StephenCleary wow - mental. Ich habe gerade einen Fehler in einer App gefunden, die sich um System.Timers.Timer dreht und am Leben bleibt und Updates veröffentlicht, nachdem ich erwartet hatte, dass sie sterben wird. Vielen Dank, dass Sie viel Zeit gespart haben!
Dr. Andrew Burnett-Thompson

Antworten:

32

Sie können diese und ähnliche Fragen mit windbg, sos und beantworten !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 beiden Fällen muss der native Timer die GC des Rückrufobjekts verhindern (über ein GCHandle). Der Unterschied besteht darin, dass im Fall System.Timers.Timerder Rückrufreferenzen das System.Timers.TimerObjekt (das intern mit a implementiert wird System.Threading.Timer)

John
quelle
10

Ich habe dieses Problem kürzlich gegoogelt, nachdem ich mir einige Beispielimplementierungen von Task.Delay angesehen und einige Experimente durchgeführt habe.

Es stellt sich heraus, ob System.Threading.Timer GCd ist oder nicht, hängt davon ab, wie Sie es konstruieren !!!

Wenn es nur mit einem Rückruf erstellt wird, ist das Statusobjekt der Timer selbst, und dies verhindert, dass es GC-fähig wird. Dies scheint nirgendwo dokumentiert zu sein und doch ist es ohne es äußerst schwierig, Feuer zu erzeugen und Timer zu vergessen.

Ich fand dies aus dem Code unter http://www.dotnetframework.org/default.aspx/DotNET/DotNET/8@0/untmp/whidbey/REDBITS/ndp/clr/src/BCL/System/Threading/Timer@cs / 1 / Timer @ cs

Die Kommentare in diesem Code geben auch an, warum es immer besser ist, den Nur-Rückruf-Ctor zu verwenden, wenn der Rückruf auf das von new zurückgegebene Timer-Objekt verweist, da sonst ein Race-Fehler auftreten könnte.

Nick H.
quelle
Es stimmt tatsächlich mit der Dokumentation überein. Wenn Sie im Rückruf einen Verweis auf System.Threading.Timer behalten (über Closure oder Konstruktor), wird dieser nicht wie ein verwaltetes Objekt erfasst.
Joe
1

In timer1 geben Sie einen Rückruf. In timer2 to schließen Sie einen Event-Handler an. Dadurch wird ein Verweis auf Ihre Programmklasse eingerichtet, was bedeutet, dass der Timer nicht GCed wird. Da Sie den Wert von timer1 nie wieder verwenden (im Grunde das gleiche, als ob Sie var timer1 = entfernt hätten), ist der Compiler intelligent genug, um die Variable zu optimieren. Wenn Sie den GC-Aufruf drücken, verweist nichts mehr auf timer1, so dass es gesammelt wird.

Fügen Sie nach Ihrem GC-Aufruf eine Console.Writeline hinzu, um eine der Eigenschaften von timer1 auszugeben, und Sie werden feststellen, dass sie nicht mehr erfasst wird.

Andy
quelle
3
Der Ereignishandler hat keinen Verweis auf die ProgramKlasse, und selbst wenn dies der Fall wäre, würde dies nicht verhindern, dass der Timer GC-fähig wird.
Stephen Cleary
3
Ja, das tut es. Kompilieren Sie den obigen Code und betrachten Sie ihn dann mit dem .Net-Reflektor. Das + = Lamba wird in eine Methode in der Program-Klasse konvertiert. Und ja, verknüpfte Ereignishandler verhindern die Speicherbereinigung. blogs.msdn.com/b/abhinaba/archive/2009/05/05/…
Andy
0

Zu Ihrer Information, ab .NET 4.6 (wenn nicht früher) scheint dies nicht mehr zuzutreffen. Wenn Ihr Testprogramm heute ausgeführt wird, führt dies nicht dazu, dass bei einem der beiden Timer Müll gesammelt wird.

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)...

Wenn ich mir die Implementierung von System.Threading.Timer anschaue , scheint dies sinnvoll zu sein, da es den Anschein hat, dass die aktuelle Version von .NET eine verknüpfte Liste aktiver Zeitgeberobjekte verwendet und diese verknüpfte Liste von einer Mitgliedsvariablen in TimerQueue (d. H. ein Singleton-Objekt, das von einer statischen Elementvariablen auch in TimerQueue am Leben erhalten wird). Infolgedessen bleiben alle Timer-Instanzen am Leben, solange sie aktiv sind.

Erv Walter
quelle
2
Ich sehe immer noch, dass die System.Threading.TimerInstanz in .NET 4.6 gesammelt wird. Stellen Sie sicher, dass Sie den Code im Release-Modus mit aktivierten Optimierungen kompilieren. Die von Ihnen erwähnte verknüpfte Liste enthält Hilfsobjekte TimerQueueTimer. Es verhindert nicht, dass die ursprüngliche System.Threading.TimerInstanz vom GC erfasst wird. (Jede System.Threading.TimerInstanz verweist auf ihr eigenes TimerQueueTimerObjekt, aber nicht umgekehrt. Wenn das System.Threading.Timervom GC erfasst wird, wird sein TimerQueueTimerObjekt vom ~TimerHolderFinalizer aus der Warteschlange entfernt .)
Antosha
Versuchen Sie es in einem Release-Modus.
Ievgen Naida
Gleiches gilt hier für die Ausführung des Codes unter .NET Core 3.0. Keiner der beiden Timer wird im Debug- oder Release-Build als Müll gesammelt. Um das Problem zu reproduzieren, muss ich die Erstellung der Timer außerhalb der MainMethode verschieben.
Theodor Zoulias