El otro día me encontré con una característica del CLR que no conocía y que provocaba un comportamiento raro en la aplicación en la que trabajo actualmente. 

La aplicación pasaba un delegado a como parámetro a un método de otra clase. Dicha clase era la encargada de recibir notificaciones de un dispositivo externo (una tableta para firmas digitales) y enviarme dichas notificaciones de regreso en el método pasado como parámetro (Callback).

Yo esperaba que mientras la otra clase recibiera puntos de la tableta esta me estaría notificando los puntos recibidos a través del delegado pasado. Bueno en “Debug” funcionaba perfectamente pero al compilar en “Release” solo recibía unos cuantos puntos (la cantidad variaba cada vez) y luego dejaba de recibir, lo cual me hacía pensar que por alguna razón la instancia de la clase dejaba de existir.

Buscando en internet encontré algunas razones para explicar este comportamiento. Para simularlo supongamos que tenemos lo siguiente:

using System;
using System.Threading;

namespace ConsoleApplication1
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            var t = new Timer(TimerCallBack, null, 0, 1000);

            Console.ReadKey();
        }

        private static void TimerCallBack(object o)
        {
            Console.WriteLine("Dentro del callback: " + DateTime.Now);

            GC.Collect();
        }
    }
}

Si se compila este código en modo Debug el método TimerCallBack será llamado cada un segundo tal y como yo (por error) esperaba. Sin embargo si se compila este código en modo “Release” veremos que el método TimerCallback solo se llama una vez.

Examinando el código podemos ver que al final del método TimerCallBack se llama a GC.Collect(); por lo que el recolector empieza su tarea de liberar la memoria ocupada por todos aquellos objetos que no vayan a ser utilizados, y como no existe ninguna referencia a t después su inicialización, el recolector asume que t no es necesario y reclama la memoria utilizada por t, por lo tanto t deja de existir y es por eso que TimerCallback solo es llamado una vez.

Pero…. ¿Porque no se comporta así si se compila en modo Debug?

Imagina por un momento que estás depurando el código anterior y que te paras en la línea 12 y quieres ver el estado de la instancia de Timer a la que t hace referencia utilizando el QuickWatch. El depurador (debugger) no te podrá mostrar el objeto por que este ha sido “destruido” por el recolector de basura, lo cual sería (según Microsoft) un comportamiento “No deseado” para los desarrolladores. 

Así que para solucionar esto, cuando el JIT compila código IL en código nativo chequea si el ensamblado fue compilado sin optimizaciones  si esto es así el JIT genera el método de tal forma que extiende la tiempo de vida de todas las variables hasta que el método finalice. Por lo que si el recolector de basura pasara recolectando todo aquello que no se va a utilizar ahora pensará que t si está referenciado para ser usado y por lo tanto no lo tocará, entonces la instancia de la clase Timer podrá seguir llamando al método TimerCallBack.

Entonces para arreglar la aplicación y que esta funcione en modo “Release” tal y como queremos, deberíamos utilizar la la variable t justo antes de que el método Main termine y para esto podríamos hacer algo así:

private static void Main(string[] args)
{
    var t = new Timer(TimerCallBack, null, 0, 1000);

    Console.ReadKey();

    t.Dispose();
}

haciendo esto conseguimos que t se necesario justo antes de la finalización del método, ya que si no, no podríamos llamar al método Dispose por lo que si ejecutamos el la aplicación ahora en modo “Release” podremos ver como el método TimerCallBack es llamado cada 1 segundo hasta que la consola detecta que una tecla ha sido presionada y con esto queda solucionado el problema.


 |