Thread Safe C # Singleton Pattern

79

Tengo algunas preguntas sobre el patrón singleton como se documenta aquí: http://msdn.microsoft.com/en-us/library/ff650316.aspx

El siguiente código es un extracto del artículo:

using System;

public sealed class Singleton
{
   private static volatile Singleton instance;
   private static object syncRoot = new object();

   private Singleton() {}

   public static Singleton Instance
   {
      get 
      {
         if (instance == null) 
         {
            lock (syncRoot) 
            {
               if (instance == null) 
                  instance = new Singleton();
            }
         }

         return instance;
      }
   }
}

Específicamente, en el ejemplo anterior, ¿es necesario comparar la instancia con nulo dos veces, antes y después del bloqueo? ¿Es esto necesario? ¿Por qué no realizar el bloqueo primero y hacer la comparación?

¿Hay algún problema en simplificar a lo siguiente?

   public static Singleton Instance
   {
      get 
      {
        lock (syncRoot) 
        {
           if (instance == null) 
              instance = new Singleton();
        }

         return instance;
      }
   }

¿Es caro realizar el bloqueo?

Wayne Phipps
fuente
17
Además, Jon Skeet tiene un artículo brillante sobre seguridad de subprocesos en Singletons: csharpindepth.com/Articles/General/Singleton.aspx
Arran
Lazy static init sería preferible ...
Mitch Wheat
1
También obtuve otros ejemplos con explicaciones aquí: csharpindepth.com/Articles/General/Singleton.aspx
Serge Voloshenko
Exactamente la misma pregunta aquí para el mundo Java.
RBT

Respuestas:

133

Realizar el bloqueo es terriblemente caro en comparación con la simple verificación del puntero instance != null.

El patrón que ve aquí se llama bloqueo de doble verificación . Su propósito es evitar la costosa operación de bloqueo que solo se necesitará una vez (cuando se accede al singleton por primera vez). La implementación es así porque también debe garantizar que cuando se inicialice el singleton no habrá errores como resultado de las condiciones de carrera del hilo.

Piénselo de esta manera: un nullcheque simple (sin a lock) está garantizado para darle una respuesta correcta utilizable solo cuando esa respuesta es "sí, el objeto ya está construido". Pero si la respuesta es "aún no construido", entonces no tiene suficiente información porque lo que realmente quería saber es que "aún no está construido y ningún otro hilo tiene la intención de construirlo en breve ". Así que usa la verificación externa como una prueba inicial muy rápida e inicia el procedimiento adecuado, libre de errores pero "costoso" (bloquear y luego verificar) solo si la respuesta es "no".

La implementación anterior es lo suficientemente buena para la mayoría de los casos, pero en este punto es una buena idea leer el artículo de Jon Skeet sobre singletons en C #, que también evalúa otras alternativas.

Jon
fuente
1
Gracias por una respuesta informativa con enlaces útiles. Muy apreciado.
Wayne Phipps
El enlace de bloqueo verificado dos veces ya no funciona.
El Mac
Lo siento, me refiero al otro.
El Mac
1
@ElMac: El sitio web de Skeet está caído ATM, volverá a funcionar a su debido tiempo. Lo tendré en cuenta y me aseguraré de que el enlace siga funcionando cuando aparezca, gracias.
Jon
3
Desde .NET 4.0, Lazy<T>este trabajo es perfecto.
ilyabreev
34

La Lazy<T>versión:

public sealed class Singleton
{
    private static readonly Lazy<Singleton> lazy
        = new Lazy<Singleton>(() => new Singleton());

    public static Singleton Instance
        => lazy.Value;

    private Singleton() { }
}

Requiere .NET 4 y C # 6.0 (VS2015) o más reciente.

andasa
fuente
Recibo "System.MissingMemberException: 'El tipo inicializado de forma perezosa no tiene un constructor público sin parámetros'". Con este código en .Net 4.6.1 / C # 6.
ttugates
@ttugates, tienes razón, gracias. Código actualizado con una devolución de llamada de fábrica de valores para el objeto perezoso.
andasa
14

Realizar un bloqueo: bastante barato (aún más caro que una prueba nula).

Realizar un bloqueo cuando otro hilo lo tiene: obtienes el costo de todo lo que aún tienen que hacer mientras bloquean, agregado a tu propio tiempo.

Realizar un bloqueo cuando otro subproceso lo tiene, y docenas de otros subprocesos también lo están esperando: paralizante.

Por motivos de rendimiento, siempre desea tener los bloqueos que otro hilo desee, durante el período de tiempo más corto posible.

Por supuesto, es más fácil razonar acerca de los bloqueos "amplios" que los estrechos, por lo que vale la pena comenzar con ellos amplios y optimizarlos según sea necesario, pero hay algunos casos que aprendemos de la experiencia y la familiaridad en los que uno más estrecho se ajusta al patrón.

(Por cierto, si puede usar private static volatile Singleton instance = new Singleton()o si puede no usar singletons sino usar una clase estática, ambos son mejores en lo que respecta a estas preocupaciones).

Jon Hanna
fuente
1
Realmente me gusta que pienses aquí. Es una excelente manera de verlo. Ojalá pudiera aceptar dos respuestas o +5 esta, muchas gracias
Wayne Phipps
2
Una consecuencia que se vuelve importante cuando llega el momento de analizar el rendimiento es la diferencia entre las estructuras compartidas que podrían verse afectadas al mismo tiempo y las que lo harán . A veces no esperamos que ese comportamiento ocurra con frecuencia, pero podría suceder, por lo que necesitamos bloquear (solo se necesita una falla para bloquear para arruinar todo). Otras veces sabemos que muchos subprocesos realmente afectarán los mismos objetos al mismo tiempo. Sin embargo, otras veces no esperábamos que hubiera mucha simultaneidad, pero nos equivocamos. Cuando necesite mejorar el rendimiento, tendrán prioridad aquellos con mucha simultaneidad.
Jon Hanna
En su alternativa, volatileno es necesario, sin embargo debería ser readonly. Consulte stackoverflow.com/q/12159698/428724 .
Wezten
7

La razón es el rendimiento. Si instance != null(que siempre será el caso, excepto la primera vez), no hay necesidad de hacer un costoso lock: dos subprocesos que acceden al singleton inicializado simultáneamente se sincronizarían innecesariamente.

Heinzi
fuente
4

En casi todos los casos (es decir: todos los casos excepto los primeros), instanceno será nulo. Adquirir un candado es más costoso que un simple cheque, por lo que verificar una vez el valor deinstance antes de bloquear es una optimización agradable y gratuita.

Este patrón se denomina bloqueo de doble verificación: http://en.wikipedia.org/wiki/Double-checked_locking

Kevin Gosse
fuente
3

Jeffrey Richter recomienda lo siguiente:



    public sealed class Singleton
    {
        private static readonly Object s_lock = new Object();
        private static Singleton instance = null;
    
        private Singleton()
        {
        }
    
        public static Singleton Instance
        {
            get
            {
                if(instance != null) return instance;
                Monitor.Enter(s_lock);
                Singleton temp = new Singleton();
                Interlocked.Exchange(ref instance, temp);
                Monitor.Exit(s_lock);
                return instance;
            }
        }
    }

Yauheni Charniauski
fuente
¿No hace que la variable de instancia sea volátil, hace lo mismo?
Ε Г И І И О
1

Podría crear con entusiasmo una instancia de Singleton segura para subprocesos, según las necesidades de su aplicación, este es un código breve, aunque preferiría la versión perezosa de @ andasa.

public sealed class Singleton
{
    private static readonly Singleton instance = new Singleton();

    private Singleton() { }

    public static Singleton Instance()
    {
        return instance;
    }
}
Brian Ogden
fuente
0

Esto se llama mecanismo de bloqueo de doble verificación, primero, verificaremos si la instancia se creó o no. Si no, solo sincronizaremos el método y crearemos la instancia. Mejorará drásticamente el rendimiento de la aplicación. Realizar el bloqueo es pesado. Entonces, para evitar el bloqueo, primero debemos verificar el valor nulo. Esto también es seguro para subprocesos y es la mejor manera de lograr el mejor rendimiento. Por favor, eche un vistazo al siguiente código.

public sealed class Singleton
{
    private static readonly object Instancelock = new object();
    private Singleton()
    {
    }
    private static Singleton instance = null;

    public static Singleton GetInstance
    {
        get
        {
            if (instance == null)
            {
                lock (Instancelock)
                {
                    if (instance == null)
                    {
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }
}
Ruta Pranaya
fuente
0

Otra versión de Singleton donde la siguiente línea de código crea la instancia de Singleton en el momento del inicio de la aplicación.

private static readonly Singleton singleInstance = new Singleton();

Aquí CLR (Common Language Runtime) se encargará de la inicialización del objeto y la seguridad de los subprocesos. Eso significa que no necesitaremos escribir ningún código explícitamente para manejar la seguridad del subproceso para un entorno multiproceso.

"La carga Eager en el patrón de diseño singleton no es un proceso en el que necesitemos inicializar el objeto singleton en el momento de la puesta en marcha de la aplicación en lugar de bajo demanda y mantenerlo listo en la memoria para usarlo en el futuro".

public sealed class Singleton
    {
        private static int counter = 0;
        private Singleton()
        {
            counter++;
            Console.WriteLine("Counter Value " + counter.ToString());
        }
        private static readonly Singleton singleInstance = new Singleton(); 

        public static Singleton GetInstance
        {
            get
            {
                return singleInstance;
            }
        }
        public void PrintDetails(string message)
        {
            Console.WriteLine(message);
        }
    }

de principal:

static void Main(string[] args)
        {
            Parallel.Invoke(
                () => PrintTeacherDetails(),
                () => PrintStudentdetails()
                );
            Console.ReadLine();
        }
        private static void PrintTeacherDetails()
        {
            Singleton fromTeacher = Singleton.GetInstance;
            fromTeacher.PrintDetails("From Teacher");
        }
        private static void PrintStudentdetails()
        {
            Singleton fromStudent = Singleton.GetInstance;
            fromStudent.PrintDetails("From Student");
        }
Jaydeep Shil
fuente
Buena alternativa, pero no responde a la pregunta sobre la verificación de bloqueo en la implementación específica mencionada en la pregunta
Wayne Phipps
no directamente, pero se puede utilizar como una alternativa "Thread Safe C # Singleton Pattern".
Jaydeep Shil
0

Patrón Singleton resistente a la reflexión:

public sealed class Singleton
{
    public static Singleton Instance => _lazy.Value;
    private static Lazy<Singleton, Func<int>> _lazy { get; }

    static Singleton()
    {
        var i = 0;
        _lazy = new Lazy<Singleton, Func<int>>(() =>
        {
            i++;
            return new Singleton();
        }, () => i);
    }

    private Singleton()
    {
        if (_lazy.Metadata() == 0 || _lazy.IsValueCreated)
            throw new Exception("Singleton creation exception");
    }

    public void Run()
    {
        Console.WriteLine("Singleton called");
    }
}
robert
fuente