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();
}
}
Perché 'timer1' è anche garbage-collected? Non è ancora in ambito? –
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. –
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. –