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?
c#
design-patterns
singleton
Wayne Phipps
fuente
fuente
Respuestas:
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
null
cheque simple (sin alock
) 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.
fuente
Lazy<T>
este trabajo es perfecto.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.
fuente
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).fuente
volatile
no es necesario, sin embargo debería serreadonly
. Consulte stackoverflow.com/q/12159698/428724 .La razón es el rendimiento. Si
instance != null
(que siempre será el caso, excepto la primera vez), no hay necesidad de hacer un costosolock
: dos subprocesos que acceden al singleton inicializado simultáneamente se sincronizarían innecesariamente.fuente
En casi todos los casos (es decir: todos los casos excepto los primeros),
instance
no 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
fuente
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; } } }
fuente
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; } }
fuente
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; } } }
fuente
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.
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"); }
fuente
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"); } }
fuente