Wygląda na to, że niektóre instancje utrzymują przy życiu instancje System.Timers.Timer
, ale instancje System.Threading.Timer
nie są.Dlaczego System.Timers.Timer przetrwał GC, ale nie System.Threading.Timer?
Przykładowy program, z okresowym System.Threading.Timer
i 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();
}
}
Kiedy uruchomić ten program (.NET 4.0 Klient, Release, poza debugger), tylko System.Threading.Timer
jest 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: mam akceptowane Jana odpowiedź poniżej, ale chciałem wykładać na nim trochę.
Uruchamiając program przykładowy powyżej (z przerwania na Sleep
), oto stan obiektów, o których mowa, a tabela 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
Jako John wskazał w swojej odpowiedzi, oba czasomierze zarejestrować swoje callback (System.Threading._TimerCallback
) w tabeli GCHandle
. Jak zauważył Hans w swoim komentarzu, parametr state
jest również utrzymywany przy życiu, kiedy to się stanie.
Jako John wskazał, powodem System.Timers.Timer
jest utrzymywane przy życiu dlatego, że odwołuje się do zwrotnego (jest on przekazywany jako parametr state
do wewnętrznej System.Threading.Timer
); podobnie, powodem, dla którego nasz System.Threading.Timer
jest GC'ed, jest , a nie, do którego odwołuje się jego wywołanie zwrotne.
Dodanie jednoznacznego odwołania do wywołania zwrotnego (np. Console.WriteLine("Stayin alive (" + timer1.GetType().FullName + ")")
) jest wystarczające, aby zapobiec GC.
Użycie konstruktora jednoparametrowego na System.Threading.Timer
działa również, ponieważ czasomierz będzie się wtedy określał jako parametr state
. Poniższy kod utrzymuje oba liczniki żywcem po GC, ponieważ każde z nich odwołuje ich zwrotnego z tabeli 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();
}
}
Dlaczego 'timer1' nawet zbiera śmieci? Czy nie jest jeszcze w zakresie? –
Jeff: Zakres nie jest naprawdę istotny. Jest to prawie raison-d'être dla metody GC.KeepAlive. Jeśli interesują Cię szczegółowe informacje, zobacz http://blogs.msdn.com/b/cbrumme/archive/2003/04/19/51365.aspx. –
Spójrz na Reflector przy ustawniku Timer.Enabled. Zauważ lewę, której używa z "ciasteczkiem", aby nadać czasomierzowi systemowemu obiekt stanu do użycia w wywołaniu zwrotnym. CLR wie o tym, clr/src/vm/comthreadpool.cpp, CorCreateTimer() w kodzie źródłowym SSCLI20. Metoda MakeDelegateInfo() jest skomplikowana. –