Perché un System.Timers.Timer sopravvive a GC ma non a System.Threading.Timer?

Sembra che le istanze System.Timers.Timer siano mantenute attive da qualche meccanismo, ma le istanze System.Threading.Timer non lo sono.

Programma di esempio, con un periodico System.Threading.Timer e reset automatico 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(); } } 

Quando eseguo questo programma (Client .NET 4.0, Release, all’esterno del debugger), solo il 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)... 

EDIT : Ho accettato la risposta di John qui sotto, ma ho voluto esporlo un po ‘.

Quando esegui il programma di esempio sopra (con un breakpoint in Sleep ), ecco lo stato degli oggetti in questione e la tabella 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 

Come John ha sottolineato nella sua risposta, entrambi i timer registrano il loro callback ( System.Threading._TimerCallback ) nella tabella GCHandle . Come Hans ha sottolineato nel suo commento, il parametro di state viene tenuto in vita anche quando viene fatto.

Come ha sottolineato John, il motivo per cui System.Timers.Timer è tenuto in vita è perché fa riferimento al callback (viene passato come parametro di state al System.Threading.Timer interno); allo stesso modo, il motivo per cui System.Threading.Timer è GC è perché non è referenziato dal suo callback.

Aggiunta di un riferimento esplicito al callback di timer1 (ad esempio, Console.WriteLine("Stayin alive (" + timer1.GetType().FullName + ")") ) è sufficiente per impedire GC.

L’utilizzo del costruttore a parametro singolo su System.Threading.Timer funziona anche, poiché il timer farà riferimento come parametro di state . Il seguente codice mantiene in vita entrambi i timer dopo il GC, poiché ciascuno di essi fa riferimento alla richiamata dalla tabella 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(); } } 

Puoi rispondere a questa e a domande simili con windbg, sos e !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 entrambi i casi, il timer nativo deve impedire il GC dell’object callback (tramite un GCHandle). La differenza è che nel caso di System.Timers.Timer il callback fa riferimento all’object System.Timers.Timer (che viene implementato internamente mediante un System.Threading.Timer )

Recentemente ho cercato su Google questo problema dopo aver esaminato alcune implementazioni di esempio di Task.Delay e facendo alcuni esperimenti.

Si scopre che System.Threading.Timer è GCd o meno dipende da come lo costruisci !!!

Se costruito con solo un callback, l’object stato sarà il timer stesso e questo impedirà che sia GC’d. Questo non sembra essere documentato ovunque e senza di esso è estremamente difficile creare il fuoco e dimenticare i timer.

Ho trovato questo dal codice a http://www.dotnetframework.org/default.aspx/DotNET/DotNET/8@0/untmp/whidbey/REDBITS/ndp/clr/src/BCL/System/Threading/Timer@cs / 1 / Timer @ cs

I commenti in questo codice indicano anche perché è sempre meglio usare il ctor di sola callback se il callback fa riferimento all’object timer restituito da new, altrimenti potrebbe esserci un bug di razza.

In timer1 lo stai richiamando. In timer2 stai collegando un gestore di eventi; questo configura un riferimento alla tua class di Programma, il che significa che il timer non sarà GCed. Dato che non si usa mai più il valore di timer1, (fondamentalmente come se si fosse rimosso il var timer1 =) il compilatore è abbastanza intelligente da ottimizzare la variabile. Quando si preme la chiamata GC, nulla sta più facendo riferimento a timer1 così da essere “raccolto”.

Aggiungi una Console.Writeline dopo la chiamata GC per generare una delle proprietà di timer1 e noterai che non è più disponibile.

Cordiali saluti, come da .NET 4.6 (se non prima), questo sembra non essere più vero. Il tuo programma di test, quando viene eseguito oggi, non comporta il fatto che il timer venga considerato come garbage collection.

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

Quando guardo all’implementazione di System.Threading.Timer , sembra logico che la versione corrente di .NET usi l’elenco collegato di oggetti timer attivi e che l’elenco collegato sia tenuto da una variabile membro all’interno di TimerQueue (che è un object singleton mantenuto in vita da una variabile membro statica anche in TimerQueue). Di conseguenza, tutte le istanze del timer verranno mantenute attive finché sono attive.