2011-02-10 11 views
60

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(); 
    } 
} 
+0

Dlaczego 'timer1' nawet zbiera śmieci? Czy nie jest jeszcze w zakresie? –

+3

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

+7

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

Odpowiedz

26

może odpowiedzieć na to i podobne pytania z WinDBG SOS i !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> 

W obu przypadkach, natywny zegar musi zapobiec GC obiektu zwrotnego (poprzez GCHandle). Różnica polega na tym, że w przypadku System.Timers.Timer callback odwołuje się do obiektu System.Timers.Timer (co jest realizowane wewnętrznie za pomocą System.Threading.Timer)

+4

+1 za nieznaną wiedzę Timer/GC i "woot" –

-3

Można użyć

GC.KeepAlive(timer1); 

aby zapobiec zbieranie śmieci na tym obiekcie.

+9

-1: Ten rodzaj odpowiedzi na niewłaściwe pytanie tutaj. Pyta, dlaczego timer, który przeżyje *, * przetrwa, a nie jak przetrwać. –

0

W czasomierzu 1 dajesz mu oddzwonienie. W czasomierzu2 do podpinania programu obsługi zdarzeń; to ustawia odniesienie do twojej klasy Programu, co oznacza, że ​​timer nie będzie GCed. Ponieważ nigdy nie używasz ponownie wartości timer1 (w zasadzie tak samo, jak w przypadku usuwania var1 timer = 1), kompilator jest wystarczająco inteligentny, aby zoptymalizować zmienną. Kiedy naciśniesz wywołanie GC, nic nie odwołuje się do timera1, więc jego "zebrane".

Dodaj Console.Writeline po wywołaniu GC, aby wyprowadzić jedną z właściwości timera1, a zauważysz, że nie jest ona już zbierana.

+3

Program obsługi zdarzeń nie ma odniesienia do klasy "Program", a nawet gdyby tak się stało, nie uniemożliwiłoby GC. –

+1

Tak właśnie jest. Skompiluj powyższy kod, a następnie spójrz na niego za pomocą reflektora .Net. Funkcja + = lamba jest konwertowana na metodę w klasie Program. I tak, łączniki zdarzeń są połączone NIE zezwalaj na zbieranie śmieci. http://blogs.msdn.com/b/abhinaba/archive/2009/05/05/memory-leak-via-event-handlers.aspx – Andy

6

I zostały googling ten problem ostatnio po pobycie w niektórych przykładowych wdrożeń Task.Delay i robi kilka eksperymentów.

Okazuje się, że to, czy System.Threading.Timer jest GCd, zależy od tego, jak go zbudujesz !!!

Jeśli zostanie skonstruowany przy użyciu tylko wywołania zwrotnego, wówczas obiekt stanu będzie sam odliczał czas, co uniemożliwi jego wygenerowanie GC. Wydaje się, że nie jest to dokumentowane gdziekolwiek, a mimo to bez niego niezwykle trudno jest stworzyć ogień i zapomnieć o czasomierzu.

Znalazłem to z kodu na http://www.dotnetframework.org/default.aspx/DotNET/DotNET/[email protected]/untmp/whidbey/REDBITS/ndp/clr/src/BCL/System/Threading/[email protected]/1/[email protected]

Komentarze w tym kodzie również wskazać, dlaczego to zawsze lepiej użyć konstruktor oddzwonienia tylko jeśli zwrotna odwołuje się do obiektu Timer zwrócony przez nowy ponieważ w przeciwnym razie nie może być bug wyścigu.

0

FYI, począwszy od .NET 4.6 (jeśli nie wcześniej), wydaje się to już nie być prawdą. Twój program testowy, po uruchomieniu dzisiaj, nie powoduje, że licznik czasu jest zbierany.

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

Jak patrzę na implementation of System.Threading.Timer, to wydaje się mieć sens, gdyż wydaje się, że obecna wersja .NET używa listy aktywnych obiektów czasowych powiązane i że związane lista jest utrzymywana przez zmienną członkiem wewnątrz TimerQueue (co jest pojedynczym obiektem utrzymywanym przy życiu przez statyczną zmienną składową również w TimerQueue). W rezultacie wszystkie instancje timera będą utrzymywane przy życiu, o ile będą aktywne.

+1

Jestem wciąż widzę instancję 'System.Threading.Timer' gromadzoną w .NET 4.6. Upewnij się, że kompilujesz kod w trybie zwolnienia z włączonymi optymalizacjami. Podana lista łączy zawiera obiekty helper 'TimerQueueTimer'; nie uniemożliwia to pobrania oryginalnej instancji 'System.Threading.Timer' przez GC. (Każda instancja 'System.Threading.Timer' odwołuje się do swojego własnego obiektu' TimerQueueTimer', ale nie odwrotnie .Gdy "System.Threading.Timer" jest zbierany przez GC, jego obiekt 'TimerQueueTimer' jest usuwany z kolejki przez '' TimerHolder' finalizer.) – Antosha

Powiązane problemy