Control doblemente bloqueado en.NET
multithreading paradigms (8)
Me encontré con este article discutiendo por qué el paradigma de bloqueo de doble verificación se rompe en Java. ¿El paradigma es válido para .NET (en particular, C #), si las variables se declaran volatile
?
He comprobado que el bloqueo funciona mediante un booleano (es decir, utilizando una primitiva para evitar la inicialización lenta):
El singleton que usa booleano no funciona. El orden de las operaciones como se ve entre los diferentes hilos no está garantizado a menos que atraviese una barrera de memoria. En otras palabras, como se ve a partir de un segundo hilo, created = true
se puede ejecutar antes instance= new Singleton();
.NET 4.0 tiene un nuevo tipo: Lazy<T>
que elimina cualquier preocupación acerca de obtener el patrón incorrecto. Es parte de la nueva Task Parallel Library.
Consulte el Centro de desarrollo de informática en paralelo de MSDN: http://msdn.microsoft.com/en-us/concurrency/default.aspx
Por cierto, hay un backport (creo que no es compatible) para .NET 3.5 SP1 disponible here .
El bloqueo de doble comprobación ahora funciona tanto en Java como en C # (el modelo de memoria de Java cambió y este es uno de los efectos). Sin embargo, tienes que hacerlo exactamente bien. Si ensucias las cosas aunque sea levemente, puedes terminar perdiendo la seguridad del hilo.
Como han indicado otras respuestas, si está implementando el patrón de singleton hay formas mucho mejores de hacerlo. Personalmente, si estoy en una situación en la que tengo que elegir entre el bloqueo doblemente comprobado y el código de "bloqueo cada vez", me iría por el bloqueo cada vez hasta que tuviera evidencia real de que estaba causando un cuello de botella. Cuando se trata de enhebrar, un patrón simple y obviamente correcto vale mucho.
He comprobado que el bloqueo funciona mediante un booleano (es decir, utilizando una primitiva para evitar la inicialización lenta):
private static Singleton instance;
private static boolean created;
public static Singleton getInstance() {
if (!created) {
synchronized (Singleton.class) {
if (!created) {
instance = new Singleton();
created = true;
}
}
}
return instance;
}
No entiendo muy bien por qué hay un montón de patrones de implementación en el bloqueo comprobado (aparentemente para solucionar las idiosincrasias del compilador en varios idiomas). El artículo de Wikipedia sobre este tema muestra el método ingenuo y las posibles formas de resolver el problema, pero ninguno es tan simple como esto (en C #):
public class Foo
{
static Foo _singleton = null;
static object _singletonLock = new object();
public static Foo Singleton
{
get
{
if ( _singleton == null )
lock ( _singletonLock )
if ( _singleton == null )
{
Foo foo = new Foo();
// Do possibly lengthy initialization,
// but make sure the initialization
// chain doesn''t invoke Foo.Singleton.
foo.Initialize();
// _singleton remains null until
// object construction is done.
_singleton = foo;
}
return _singleton;
}
}
En Java, usaría synchronized () en lugar de lock (), pero básicamente es la misma idea. Si existe la posible incoherencia de cuándo se asigna el campo singleton, ¿por qué no utilizar primero una variable de ámbito local y luego asignar el campo singleton en el último momento posible antes de salir de la sección crítica? ¿Me estoy perdiendo de algo?
Existe el argumento de @michael-borgwardt de que en C # y Java el campo estático solo se inicializa una vez en el primer uso, pero ese comportamiento es específico del idioma. Y he usado este patrón con frecuencia para la inicialización lenta de una propiedad de colección (por ejemplo, user.Sessions).
No entiendo por qué todas las personas dicen que el doble bloqueo es un mal patrón, pero no adaptas el código para que funcione correctamente. En mi opinión, este código a continuación debería funcionar bien.
Si alguien pudiera decirme si este código sufre el problema mencionado en el artículo de Cameron, por favor hazlo.
public sealed class Singleton {
static Singleton instance = null;
static readonly object padlock = new object();
Singleton() {
}
public static Singleton Instance {
get {
if (instance != null) {
return instance;
}
lock (padlock) {
if (instance != null) {
return instance;
}
tempInstance = new Singleton();
// initialize the object with data
instance = tempInstance;
}
return instance;
}
}
}
Tenga en cuenta que en Java (y muy probablemente en .Net también), el bloqueo con doble verificación para la inicialización de singleton es completamente innecesario y está roto. Como las clases no se inicializan hasta que se usan por primera vez, la inicialización lenta deseada ya se logra con esto;
private static Singleton instance = new Singleton();
A menos que su clase Singleton contenga elementos como constantes a los que se pueda acceder antes de que se use por primera vez una instancia de Singleton, esto es todo lo que necesita hacer.
La implementación del patrón Singleton en C # habla sobre este problema en la tercera versión.
Dice:
Hacer que la variable de instancia sea volátil puede hacer que funcione, al igual que las llamadas explícitas de barrera a la memoria, aunque en este último caso incluso los expertos no pueden acordar exactamente qué barreras son necesarias. ¡Tiendo a tratar de evitar situaciones en las que los expertos no están de acuerdo con lo que está bien y lo que está mal!
El autor parece dar a entender que es menos probable que el doble bloqueo funcione que otras estrategias y, por lo tanto, no debería utilizarse.