2011-02-10 10 views
60

Sembra che le istanze System.Timers.Timer siano mantenute attive da qualche meccanismo, ma le istanze System.Threading.Timer non lo sono.Perché un System.Timers.Timer sopravvive a GC ma non a System.Threading.Timer?

Programma di esempio, con una periodica System.Threading.Timer e 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(); 
    } 
} 

Quando ho eseguito questo programma (NET 4.0 client, di rilascio, al di fuori del debugger), solo il 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)... 

EDIT: ho accettato la risposta di Giovanni sotto, ma ho voluto esporre su di esso un po '.

Quando si esegue il programma di esempio sopra (con un punto di interruzione 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 registrare la loro callback (System.Threading._TimerCallback) nella tabella GCHandle. Come ha sottolineato Hans nel suo commento, anche il parametro state viene tenuto in vita quando viene eseguito.

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

L'aggiunta di un riferimento esplicito alla richiamata timer1 (ad esempio) è sufficiente per impedire GC.

L'utilizzo del costruttore a parametro singolo su System.Threading.Timer funziona anche, poiché il timer farà riferimento come parametro state. Il seguente codice mantiene entrambi i timer vivo dopo il GC, dal momento che ciascuno di essi è fatto riferimento per la loro 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(); 
    } 
} 
+0

Perché 'timer1' è anche garbage-collected? Non è ancora in ambito? –

+3

Jeff: Scope non è davvero rilevante. Questo è praticamente il ragionamento per il metodo GC.KeepAlive. Se sei interessato ai dettagli schizzinosi, consulta http://blogs.msdn.com/b/cbrumme/archive/2003/04/19/51365.aspx. –

+7

Dai un'occhiata a Reflector nel timer. Abilita setter. Nota il trucco che usa con "cookie" per dare al timer di sistema un oggetto di stato da usare nel callback. Il CLR ne è a conoscenza, clr/src/vm/comthreadpool.cpp, CorCreateTimer() nel codice sorgente SSCLI20. MakeDelegateInfo() si complica. –

risposta

26

è possibile rispondere a questa e simili domande 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 ha per evitare GC dell'oggetto callback (tramite un GCHandle). La differenza è che nel caso di System.Timers.Timer callback fa riferimento all'oggetto System.Timers.Timer (che viene implementato internamente con un System.Threading.Timer)

+4

+1 per l'oscuro conoscenza di Timer/GC e "woot" –

-3

È possibile utilizzare

GC.KeepAlive(timer1); 

per impedire la raccolta dei rifiuti su questo oggetto.

+9

-1: questo tipo di risposte alla domanda sbagliata qui. Chiede perché il timer sopravvive * sopravvive, non come far sopravvivere l'altro. –

0

In timer1 si sta dando un richiamo. In timer2 stai collegando un gestore di eventi; questo configura un riferimento alla tua classe 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.

+3

Il gestore di eventi non ha un riferimento alla classe 'Program', e anche se lo avesse fatto, non avrebbe impedito che il timer venisse eseguito da GC. –

+1

Sì. Compilare il codice sopra e poi guardarlo con .Net reflector. + = Lamba viene convertito in un metodo nella classe Program. E sì, i gestori di eventi che sono collegati DO impediscono la garbage collection. http://blogs.msdn.com/b/abhinaba/archive/2009/05/05/memory-leak-via-event-handlers.aspx – Andy

6

mi sono stati Googling questo problema di recente dopo aver guardato alcuni esempi di implementazioni di Task.Delay e di fare alcuni esperimenti.

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

Se costruito con solo un callback quindi l'oggetto di stato sarà il timer stesso e ciò impedirà che venga 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/[email protected]/untmp/whidbey/REDBITS/ndp/clr/src/BCL/System/Threading/[email protected]/1/[email protected]

I commenti in questo codice di indicare anche il motivo per cui è sempre meglio usare il ctor richiamata solo se il callback fa riferimento all'oggetto del timer restituito dal nuovo altrimenti ci potrebbe essere un bug di razza.

0

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

Mentre guardo il implementation of System.Threading.Timer, questo sembra avere senso in quanto sembra che la versione corrente di .NET utilizza elenco di oggetti di timer attivi collegata e che lista collegata è detenuto da una variabile membro all'interno TimerQueue (che è un oggetto 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.

+1

I am continua a vedere l'istanza 'System.Threading.Timer' in fase di raccolta in .NET 4.6. Assicurati di compilare il codice in modalità di rilascio con le ottimizzazioni attivate. La lista collegata che hai citato contiene oggetti helper 'TimerQueueTimer'; non impedisce che l'istanza originale 'System.Threading.Timer' venga raccolta dal GC. (Ogni istanza di 'System.Threading.Timer' fa riferimento al proprio oggetto' TimerQueueTimer', ma non viceversa.Quando il 'System.Threading.Timer' viene raccolto dal GC, il suo oggetto' TimerQueueTimer' viene rimosso dalla coda dal Finalizzatore '~ TimerHolder'.) – Antosha

Problemi correlati