El patrón Singleton en C#

Una de las cosas que creo es importante cuando usas un lenguaje es entender como funciona por dentro. Por ejemplo es cierto que C# se parece mucho a Java pero a bajo nivel presentan muchas diferencias de filosofía y diseño que creo son importantes de entender para hacer un uso eficaz del mismo. Algo similar ocurre cuando alguien que está acostumbrado a C++ se pasa a C#, muchas veces piensa que es un C++ “fácil” (sin punteros, sin gestión de memoria) y luego se queja de que su rendimiento es mucho peor, que va lento,… cuando a veces todo esto es porque está usando C# como si fuera C++ y las cosas no funcionan así.

Un ejemplo de libro de esta situación es la implementación del patrón Singleton en C#. Si buscáis un poco, en muchas páginas webs para implementar un Singleton lazy y thread-safe en Java/C++ se recomienda la siguiente implementación:

public sealed class Singleton

{

    static Singleton instance = null;

    static readonly object padlock = new object();

    Singleton()

    {

    }

    public static Singleton Instance

    {

        get

        {

            if (instance == null)

            {

                lock (padlock)

                {

                    if (instance == null)

                    {

                        instance = new Singleton();

                    }

                }

            }

            return instance;

        }

    }

}

La sintaxis es de C#, pero lo importante es la idea del if/lock/if. A esto se le llama double-checked-locking y por motivos bastante sutiles esa implementación no es totalmente thread-safe (más detalles en este paper). El problema de thread-safe se puede solucionar haciendo la variable instance volatile (a partir de la JDK 5.0 y Visual C++ 2005, ni idea en otros compiladores de C++).

El problema es que como mucha gente de Java/C++ se ha acostumbrado a implementar el Singleton de esa manera, cuando llegan a C# hacen lo mismo. Pero resulta que en C# la implementación correcta es la siguiente:

public sealed class Singleton

{

    static readonly Singleton instance = new Singleton();

    static Singleton()

    {

    }

    Singleton()

    {

    }

    public static Singleton Instance

    {

        get

        {

            return instance;

        }

    }

}

Visto así por encima, esa implementación no parece ni lazy, ni thread-safe, ni nada de nada. Pero si nos cogemos el gran CLR via C# (libro totalmente recomendado) podemos entender como es que tan poco código hace tantas cosas. Toda la implementación gira en torno a los constructores estáticos y como funcionan dentro de .NET. Lo primero es entender que esto:

public sealed class Singleton

{

    static int a = 5;

}

En IL se traduce a esto:

public sealed class Singleton

{

    static int a;

    static Singleton()

    {

        a = 5;

    }

}

Y que por ejemplo esto otro:

public sealed class Singleton

{

    static int a = 5;

    static int b;

    static Singleton()

    {

        b = 10;

    }

}

En IL queda así:

public sealed class Singleton

{

    static int a;

    static int b;

    static Singleton()

    {

        a = 5;

        b = 10;

    }

}

Vamos, que las inicializaciones de variables estáticas una vez compilada la clase se incluyen dentro de un constructor estático (si el constructor existe se añaden al principio en el orden que son declaradas, si no existe se genera un constructor y se añaden).

En C#, cuando el compilador JIT (el que pasa de IL a código máquina) encuentra un constructor estático, mira si ese constructor ya se ha ejecutado o no:

  • Si no se ha ejecutado, emite un lock, el código de ejecución y un unlock.
  • Si se ha ejecutado, no emite nada.

Es decir, el JIT se asegura que el constructor estático solo se ejecute una vez aunque haya varios hilos, por eso en C# no hace falta poner el lock, ya que la línea new Singleton() se ejecuta dentro del constructor estático y el propio JIT ya se ha encargado que ese constructor solo sea llamado una vez. Con esto ya tenemos cubierto el tema de thread-safe, pero ¿y lo de que sea lazy?

Primero, tengo que reconocer que os he mentido un poco antes. Esta clase:

public sealed class Singleton

{

    static int a = 5;

}

Y esta otra clase:

public sealed class Singleton

{

    static int a;

    static Singleton()

    {

        a = 5;

    }

}

No generan exactamente el mismo IL: la primera está marcada con un atributo que se llama BeforeFieldInit y la segunda no. Cuando una clase está marcada como BeforeFieldInit el runtime del framework puede decidir ejecutar su inicializador de tipo (constructor estático) cuando se haga la primera referencia a ese tipo o antes, según le parezca. Pero si no está marcada con BeforeFieldInit (porque hemos puesto un constructor estático explícito) entonces el inicializador del tipo solo puede ejecutarse en el momento que se haga uso de ese tipo.

Es decir, como mi implementación del Singleton tiene un constructor estático, esta clase no está marcada como BeforeFieldInit y lo que haya dentro del constructor estático (new Singleton()) se ejecutará justo cuando haga Singleton.Instance. Pero si no tuviera un constructor estático la línea new Singleton() se podría ejecutar una hora antes de llamar a Singleton.Instance (por ejemplo), según decida el runtime. Y esto puede ser un problema porque no tenemos ni idea de cuando se va a ejecutar ese constructor y si tiene código muy pesado puede ralentizar el resto del programa en algún punto que no sea adecuado, o incluso ejecutarse y que luego resulte que nunca se utiliza en el programa.

Podéis encontrar una discusión mucho más en profundidad del tema en este artículo y este otro (cuentan lo mismo pero de forma mucho más técnica).

Actualización: Reed Copsey ha publicado un artículo sobre este mismo tema llamado "Just keep repeating: C# is not Java. C# is not C++" que contiene más ejemplos y resulta bastante interesante.