Singleton y Lazy Loading en .NET 4

Singleton y Lazy Loading en .NET 4Hace tiempo me construí una clase Logger a partir de la de un compañero, añadiendo un par de detalles para adaptarla a mis necesidades. Es una clase muy útil para procesos que se ejecutan en consola o servicios de Windows o WCF. Se instancia de forma estática utilizando el patrón Singleton, de modo que cualquier proceso de la aplicación puede acceder a ella y escribir tanto en consola como en un archivo de texto lo que vamos haciendo. Llevo ya bastante tiempo usándola y nunca me ha dado problemas.

Pero el otro día otro compañero que la está utilizando me planteó una duda: le daba la sensación de que la clase estaba ralentizando la ejecución de su aplicación, la cual utilizaba varios subprocesos para realizar ejecuciones bastante costosas. Yo le comenté que había utilizado mi Logger con aplicaciones que también usaba múltiples hilos e incluso con la Task Parallel Library de .NET 4 y nunca había notado nada raro.

Aún así, me quedó la duda: ¿Es totalmente thread-safe una clase declarada mediante el patrón Singleton?

La respuesta, tras consultar un foro de debate en LinkedIn y un par de blogs, es que… ¿lo adivináis? Depende de vuestra implementación :-) Veámoslo en detalle.

Primera aproximación: El patrón Singleton

Doy por hecho que conoceréis el patrón singleton, ya que es una de los primeros conceptos que se aprenden cuando empiezas a programar. Pero para refrescar la memoria, consiste en diseñar la implementación de una clase para que se cree una única instancia de la misma en toda la aplicación. Aunque hay quien lo llega a considerar un anti patrón por el hecho de depender del uso de métodos y propiedades estáticas y dificultar el unit testing, yo pienso que puede ser muy útil en ciertos casos, siempre que no se abuse de él. Mi clase Logger puede ser un ejemplo perfecto, ya que aseguramos que solamente hay un objeto en toda la aplicación que utiliza un recurso de disco: el archivo de texto en el que se imprime el log.

¿El problema de Singleton? Que no es Thread Safe. Un ejemplo de una mala implementación del patrón Singleton sería la siguiente:

// Mala implementación!!!
public sealed class MiClase
{
    private static MiClase instance=null;
 
    private MiClase()
    {
    }
 
    public static MiClase Instance
    {
        get
        {
            if (instance==null)
            {
                instance = new MiClase();
            }
            return instance;
        }
    }
}

 

En el ejemplo anterior, no es thread safe, ya que dos procesos de la aplicación podrían evaluar al mismo tiempo que (instance == null) = true y generar una nueva instancia de MiClase.

Segunda aproximación: uso de bloqueos (lock)

Si el problema es la concurrencia, lo primero que nos viene a la cabeza es utilizar un lock (bloqueo) para proteger el código que genera la instancia del Singleton (o de cualquiera de sus métodos). El lock asegura una barrera de memoria, de forma que solo un proceso puede ejecutar esa porción de código al mismo tiempo hasta que se libera.

public sealed class MiClase
{
    private static MiClase instance = null;
    private static readonly object padlock = new object();
 
    MiClase()
    {
    }
 
    public static MiClase Instance
    {
        get
        {
            lock (padlock)
            {
                if (instance == null)
                {
                    instance = new MiClase();
                }
                return instance;
            }
        }
    }
}

La implementación anterior soluciona el problema a medias, ya que genera otros nuevos. Si os fijáis, una clase externa que quiera instanciar MiClase, lo haría así:

MiClase.Instance;

De forma que cada vez que se accede a la propiedad Instance se produce un bloqueo. Y los bloqueos son peligrosos, ya que pueden dejar “frita” nuestra aplicación dependiendo del código que tengan que ejecutar. Así que no es una mala solución en la mayoría de los casos, pero prefiero la siguiente.

Tercera aproximación: ¡Fuera bloqueos!

En C# un constructor estático cuando se crea una instancia de la clase o uno de sus miembros estáticos es referenciado, y se ejecuta una sola vez por dominio de aplicación (AppDomain). Dicho esto, con la siguiente implementación aseguramos que la instancia de MiClase se ejecutará una vez pase lo que pase, sin tener que recurrir a comprobaciones o a bloqueos y optimizando al máximo el rendimiento.

public sealed class MiClase
{
    private static readonly MiClase instance = new MiClase();
 
    static MiClase()
    {
    }
 
    private MiClase()
    {
    }
 
    public static MiClase Instance
    {
        get
        {
            return instance;
        }
    }
}

Cuarta aproximación: ¡Ponte al día y usa Lazy Loading de .NET 4!

Si los ejemplos anteriores eran bastante estándar e incluso independientes del lenguaje, este es solo para programadores de .NET 4. Pero como es mi caso… ya podéis adivinar que esta última solución es mi preferida. Siempre digo que cuando Microsoft inventa algo, no hay por qué complicarse matando moscas a cañonazos.

A partir de la versión 4 del framework se nos ha incluído la clase System.Lazy<T>, con la que podemos construir de forma “perezosa” y completamente thread-safe un objeto de tipo T.

public sealed class MiClase
{
    private static readonly Lazy<MiClase> lazy =
        new Lazy<MiClase>(() => new MiClase());
 
    public static MiClase Instance { get { return lazy.Value; } }
 
    private MiClase()
    {
    }
}

El constructor requiere un delegate para construir el objeto de tipo MiClase, algo que se soluciona en una línea con una simple lambda expression. La solución no utiliza bloqueos y asegura no solo una única instancia, sino también una “carga perezosa”, que es otro de los motivos por los que alguien querría utilizar un Singleton. Además, tiene varias propiedades y métodos muy útiles para gestionar la instancia, como

  • IsValueCreated, que nos dice si el tipo está inicializado
  • Value, que nos devuelve la instancia de tipo T
  • Finalize() que ejecuta la instrucción Finalize() del IDisposable y libera recursos de memoria
  • Y otros que podéis consultar en la especificación (ver)

En mi opinión, este último ejemplo es óptimo en cuanto a claridad del código y rendimiento.

Conclusión

Espero que os haya sido útil el artículo y que no le perdáis el miedo a los Singleton, muy denostados últimamente. Como siempre, si tenéis alguna sugerencia para mejorar el código, los comentarios son bienvenidos :)

Acerca de dandel

Joven y entusiasta programador al que le apasiona su trabajo. Java, PHP, VB, C#, CSS, jQuery, Lucene o Photoshop, no hay código que se me resista o herramienta que no sea capaz de dominar bajo el yugo de mi perseverancia.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos necesarios están marcados *

*