.net timer garbage-collection

.net - ¿Por qué System.Timers.Timer sobrevive a GC pero no a System.Threading.Timer?



garbage-collection (4)

Parece que las instancias System.Timers.Timer se mantienen activas mediante algún mecanismo, pero las instancias System.Threading.Timer no lo son.

Programa de ejemplo, con un sistema periódico.Threading.Timer y 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(); } }

Cuando ejecuto este programa (.NET 4.0 Client, Release, fuera del depurador), solo el System.Threading.Timer es 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)...

EDITAR : He aceptado la respuesta de John a continuación, pero quería exponer un poco.

Al ejecutar el programa de ejemplo anterior (con un punto de interrupción en Sleep ), aquí está el estado de los objetos en cuestión y la tabla 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

Como señaló John en su respuesta, ambos temporizadores registran su devolución de llamada ( System.Threading._TimerCallback ) en la tabla GCHandle . Como señaló Hans en su comentario, el parámetro de state también se mantiene vivo cuando se hace esto.

Como lo señaló John, la razón por la cual System.Timers.Timer se mantiene activo es porque se hace referencia a través de la devolución de llamada (se pasa como parámetro de state al System.Threading.Timer interno); Del mismo modo, la razón por la que nuestro System.Threading.Timer está activado por GC es porque no se hace referencia a su devolución de llamada.

Agregar una referencia explícita a la devolución de llamada del timer1 (por ejemplo, Console.WriteLine("Stayin alive (" + timer1.GetType().FullName + ")") ) es suficiente para evitar GC.

Usar el constructor de parámetro único en System.Threading.Timer también funciona, porque el temporizador se referirá a sí mismo como el parámetro de state . El siguiente código mantiene ambos temporizadores activos después del GC, ya que cada uno de ellos se referencia por su devolución de llamada de la tabla 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(); } }


En timer1 le estás dando una devolución de llamada. En timer2 estás conectando un controlador de eventos; esto configura una referencia a su clase de programa, lo que significa que el temporizador no será GCed. Como nunca se vuelve a utilizar el valor de timer1 (básicamente como si se hubiera eliminado var timer1 =), el compilador es lo suficientemente inteligente como para optimizar la variable. Cuando presionas la llamada GC, nada hace referencia al timer1, así que es ''recopilado''.

Agregue Console.Writeline después de su llamada a GC para generar una de las propiedades de timer1 y notará que ya no se recopila.


FYI, a partir de .NET 4.6 (si no antes), parece que ya no es así. Su programa de prueba, cuando se ejecuta hoy, no da como resultado que el temporizador sea basura.

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

Cuando veo la implementación de System.Threading.Timer , parece tener sentido, ya que parece que la versión actual de .NET utiliza una lista vinculada de objetos de temporizador activos y esa lista vinculada está en manos de una variable miembro dentro de TimerQueue (que es un objeto singleton mantenido vivo por una variable miembro estática también en TimerQueue). Como resultado, todas las instancias del temporizador se mantendrán activas mientras estén activas.


He estado buscando en Google este tema recientemente después de ver algunas implementaciones de ejemplo de Task.Delay y hacer algunos experimentos.

¡Resulta que si System.Threading.Timer es o no GCd depende de cómo lo construyas!

Si se construye con solo una devolución de llamada, entonces el objeto de estado será el temporizador mismo y esto evitará que sea GC''d. Esto no parece estar documentado en ninguna parte y, sin embargo, sin él es extremadamente difícil crear fuego y olvidar temporizadores.

Encontré esto en el código en http://www.dotnetframework.org/default.aspx/DotNET/DotNET/8@0/untmp/whidbey/REDBITS/ndp/clr/src/BCL/System/Threading/Timer@cs/1/Timer@cs

Los comentarios en este código también indican por qué siempre es mejor usar el ctor de solo devolución de llamada si la devolución de llamada hace referencia al objeto del temporizador devuelto por nuevo ya que de lo contrario podría haber un error de carrera.


Puede responder a esta y otras preguntas similares con windbg, sos y !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>

En ambos casos, el temporizador nativo tiene que evitar la GC del objeto de devolución de llamada (a través de un GCH y una). La diferencia es que, en el caso de System.Timers.Timer la devolución de llamada hace referencia al objeto System.Timers.Timer (que se implementa internamente mediante System.Threading.Timer ).