Varios textos dicen que al implementar el bloqueo doble verificado en .NET, el campo que está bloqueando debe tener aplicado un modificador volátil. ¿Pero por qué exactamente? Considerando el siguiente ejemplo:
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;
}
}
}
¿Por qué "lock (syncRoot)" no logra la consistencia de memoria necesaria? ¿No es cierto que después de la instrucción "lock" tanto la lectura como la escritura serían volátiles y, por lo tanto, se lograría la consistencia necesaria?
Respuestas:
Volátil es innecesario. Especie de**
volatile
se utiliza para crear una barrera de memoria * entre lecturas y escrituras en la variable.lock
, cuando se usa, hace que se creen barreras de memoria alrededor del bloque dentro dellock
, además de limitar el acceso al bloque a un hilo.Las barreras de memoria hacen que cada hilo lea el valor más actual de la variable (no un valor local almacenado en caché en algún registro) y que el compilador no reordene las declaraciones. Usar
volatile
es innecesario ** porque ya tienes un candado.Joseph Albahari explica estas cosas mucho mejor que yo.
Y asegúrese de consultar la guía de Jon Skeet para implementar el singleton en C #
update :
*
volatile
hace que las lecturas de la variable seanVolatileRead
sy las escrituras seanVolatileWrite
s, que en x86 y x64 en CLR, se implementan con unMemoryBarrier
. Pueden tener un grano más fino en otros sistemas.** mi respuesta solo es correcta si está utilizando CLR en procesadores x86 y x64. Se podría ser cierto en otros modelos de memoria, al igual que en Mono (y otras implementaciones), Itanium64 y hardware en el futuro. Esto es a lo que Jon se refiere en su artículo sobre las "trampas" para el bloqueo doble verificado.
Puede ser necesario {marcar la variable como
volatile
, leerlaThread.VolatileRead
o insertar una llamada aThread.MemoryBarrier
} para que el código funcione correctamente en una situación de modelo de memoria débil.Por lo que entiendo, en CLR (incluso en IA64), las escrituras nunca se reordenan (las escrituras siempre tienen semántica de liberación). Sin embargo, en IA64, las lecturas se pueden reordenar para que sean anteriores a las escrituras, a menos que estén marcadas como volátiles. Desafortunadamente, no tengo acceso al hardware IA64 para jugar, así que cualquier cosa que diga al respecto sería una especulación.
También he encontrado útiles estos artículos:
http://www.codeproject.com/KB/tips/MemoryBarrier.aspx artículo de
vance morrison (todo enlaza con esto, habla de bloqueo doble verificado)
artículo de chris brumme (todo enlaza con esto )
Joe Duffy: Variantes rotas de bloqueo con doble verificación
La serie de luis abreu sobre subprocesos múltiples también ofrece una buena descripción general de los conceptos
http://msmvps.com/blogs/luisabreu/archive/2009/06/29/multithreading-load-and-store-reordering.aspx
http: // msmvps. com / blogs / luisabreu / archive / 2009/07/03 / multithreading-introducing-memory-fences.aspx
fuente
volatile
no era necesaria en cualquier plataforma entonces significaría el JIT no podía optimizar las cargas de memoriaobject s1 = syncRoot; object s2 = syncRoot;
aobject s1 = syncRoot; object s2 = s1;
en esa plataforma. Eso me parece muy poco probable.Hay una forma de implementarlo sin
volatile
campo. Te lo explicare ...Creo que es el reordenamiento del acceso a la memoria dentro de la cerradura lo que es peligroso, de modo que puede obtener una instancia no completamente inicializada fuera de la cerradura. Para evitar esto, hago esto:
public sealed class Singleton { private static Singleton instance; private static object syncRoot = new Object(); private Singleton() {} public static Singleton Instance { get { // very fast test, without implicit memory barriers or locks if (instance == null) { lock (syncRoot) { if (instance == null) { var temp = new Singleton(); // ensures that the instance is well initialized, // and only then, it assigns the static variable. System.Threading.Thread.MemoryBarrier(); instance = temp; } } } return instance; } } }
Entendiendo el código
Imagina que hay algún código de inicialización dentro del constructor de la clase Singleton. Si estas instrucciones se reordenan después de establecer el campo con la dirección del nuevo objeto, entonces tienes una instancia incompleta ... imagina que la clase tiene este código:
private int _value; public int Value { get { return this._value; } } private Singleton() { this._value = 1; }
Ahora imagina una llamada al constructor usando el nuevo operador:
instance = new Singleton();
Esto se puede ampliar a estas operaciones:
ptr = allocate memory for Singleton; set ptr._value to 1; set Singleton.instance to ptr;
¿Qué pasa si reordeno estas instrucciones como esta?
ptr = allocate memory for Singleton; set Singleton.instance to ptr; set ptr._value to 1;
¿Hace alguna diferencia? NO si piensas en un solo hilo. SÍ, si piensa en varios hilos ... ¿qué pasa si el hilo se interrumpe justo después de
set instance to ptr
:ptr = allocate memory for Singleton; set Singleton.instance to ptr; -- thread interruped here, this can happen inside a lock -- set ptr._value to 1; -- Singleton.instance is not completelly initialized
Eso es lo que evita la barrera de la memoria, al no permitir el reordenamiento del acceso a la memoria:
ptr = allocate memory for Singleton; set temp to ptr; // temp is a local variable (that is important) set ptr._value to 1; -- memory barrier... cannot reorder writes after this point, or reads before it -- -- Singleton.instance is still null -- set Singleton.instance to temp;
¡Feliz codificación!
fuente
new
está totalmente inicializado ..No creo que nadie haya respondido realmente a la pregunta , así que lo intentaré.
Los volátiles y los primeros
if (instance == null)
no son "necesarios". El bloqueo hará que este código sea seguro para subprocesos.Entonces la pregunta es: ¿por qué agregarías el primero
if (instance == null)
?La razón es presumiblemente para evitar ejecutar la sección bloqueada de código innecesariamente. Mientras está ejecutando el código dentro del bloqueo, cualquier otro hilo que intente ejecutar también ese código se bloquea, lo que ralentizará su programa si intenta acceder al singleton con frecuencia desde muchos hilos. Dependiendo del idioma / plataforma, también puede haber gastos generales de la cerradura que desea evitar.
Entonces, la primera verificación nula se agrega como una forma realmente rápida de ver si necesita el bloqueo. Si no necesita crear el singleton, puede evitar el bloqueo por completo.
Pero no puede verificar si la referencia es nula sin bloquearla de alguna manera, porque debido al almacenamiento en caché del procesador, otro hilo podría cambiarla y leería un valor "obsoleto" que lo llevaría a ingresar el bloqueo innecesariamente. ¡Pero estás intentando evitar un candado!
Por lo tanto, hace que el singleton sea volátil para asegurarse de leer el valor más reciente, sin necesidad de usar un candado.
Aún necesita el candado interno porque volátil solo lo protege durante un acceso único a la variable; no puede probarlo y configurarlo de manera segura sin usar un candado.
Ahora bien, ¿es esto realmente útil?
Bueno, yo diría "en la mayoría de los casos, no".
Si Singleton.Instance podría causar ineficiencia debido a las cerraduras, entonces ¿por qué lo llama con tanta frecuencia que esto sería un problema importante ? El objetivo de un singleton es que solo hay uno, por lo que su código puede leer y almacenar en caché la referencia singleton una vez.
El único caso en el que puedo pensar en el que este almacenamiento en caché no sería posible sería cuando tiene una gran cantidad de subprocesos (por ejemplo, un servidor que usa un nuevo subproceso para procesar cada solicitud podría estar creando millones de subprocesos de ejecución muy corta, cada uno de los cuales que tendría que llamar a Singleton.Instance una vez).
Así que sospecho que el bloqueo con doble verificación es un mecanismo que tiene un lugar real en casos muy específicos de rendimiento crítico, y luego todos se han subido al tren de "esta es la forma correcta de hacerlo" sin pensar realmente qué hace y si será realmente necesario en el caso de que lo estén usando.
fuente
volatile
no tiene nada que ver con la semántica de bloqueo en el bloqueo doblemente verificado, tiene que ver con el modelo de memoria y la coherencia de la caché. Su propósito es garantizar que un subproceso no reciba un valor que todavía esté siendo inicializado por otro subproceso, que el patrón de bloqueo de doble verificación no previene de forma inherente. En Java definitivamente necesitas lavolatile
palabra clave; en .NET es turbio, porque está mal según ECMA pero correcto según el tiempo de ejecución. De cualquier manera,lock
definitivamente no se ocupa de eso.lock
hace que el código sea seguro para subprocesos. Esa parte es cierta, pero el patrón de bloqueo de doble verificación puede hacerlo inseguro . Eso es lo que parece que te estás perdiendo. Esta respuesta parece divagar sobre el significado y el propósito de un bloqueo de doble verificación sin abordar los problemas de seguridad de los subprocesos que son la razónvolatile
.instance
está marcado convolatile
?Debe usar volátil con el patrón de bloqueo de doble verificación.
La mayoría de las personas señalan este artículo como prueba de que no necesita volátiles: https://msdn.microsoft.com/en-us/magazine/cc163715.aspx#S10
Pero no logran leer hasta el final: " Una última advertencia: solo estoy adivinando el modelo de memoria x86 a partir del comportamiento observado en los procesadores existentes. Por lo tanto, las técnicas de bloqueo bajo también son frágiles porque el hardware y los compiladores pueden volverse más agresivos con el tiempo . Aquí hay algunas estrategias para minimizar el impacto de esta fragilidad en su código. Primero, siempre que sea posible, evite las técnicas de bloqueo bajo. (...) Finalmente, asuma el modelo de memoria más débil posible, utilizando declaraciones volátiles en lugar de confiar en garantías implícitas . "
Si necesita más convencimiento, lea este artículo sobre la especificación ECMA que se utilizará para otras plataformas: msdn.microsoft.com/en-us/magazine/jj863136.aspx
Si necesita más convencimiento, lea este artículo más reciente de que se pueden implementar optimizaciones que eviten que funcione sin volátiles: msdn.microsoft.com/en-us/magazine/jj883956.aspx
En resumen, "podría" funcionar para usted sin volatile por el momento, pero no se arriesgue a que escriba el código adecuado y utilice los métodos volatile o volatileread / write. Los artículos que sugieren hacer lo contrario a veces omiten algunos de los posibles riesgos de las optimizaciones del compilador / JIT que podrían afectar su código, así como las optimizaciones futuras que pueden ocurrir y que podrían romper su código. Además, como se mencionó en las suposiciones del artículo anterior, las suposiciones anteriores de trabajar sin volátiles ya pueden no ser válidas para ARM.
fuente
AFAIK (y - tómese esto con precaución, no estoy haciendo muchas cosas concurrentes) no. El bloqueo solo le brinda sincronización entre múltiples contendientes (hilos).
volatile por otro lado le dice a su máquina que reevalúe el valor cada vez, para que no tropiece con un valor en caché (y equivocado).
Consulte http://msdn.microsoft.com/en-us/library/ms998558.aspx y observe la siguiente cita:
Una descripción de volatile: http://msdn.microsoft.com/en-us/library/x13ttww7%28VS.71%29.aspx
fuente
Creo que encontré lo que buscaba. Los detalles se encuentran en este artículo: http://msdn.microsoft.com/en-us/magazine/cc163715.aspx#S10 .
En resumen, el modificador volátil .NET no es necesario en esta situación. Sin embargo, en modelos de memoria más débiles, las escrituras realizadas en el constructor de un objeto iniciado de forma diferida pueden retrasarse después de escribir en el campo, por lo que otros subprocesos pueden leer una instancia corrupta no nula en la primera instrucción if.
fuente
El
lock
es suficiente. La especificación de lenguaje de MS (3.0) en sí misma menciona este escenario exacto en §8.12, sin ninguna mención devolatile
:fuente
Esta es una publicación bastante buena sobre el uso de volátiles con bloqueo de doble verificación:
http://tech.puredanger.com/2007/06/15/double-checked-locking/
En Java, si el objetivo es proteger una variable, no es necesario bloquear si está marcada como volátil
fuente