Yo tenía entendido que .NET modelo de memoria (al menos desde 2.0) no ha requerido que la instancia se declare como volátil, ya que el bloqueo proporcionaría una valla de memoria completa. ¿No es ese el caso, o estaba yo mal informado?
Es obligatorio. La razón es porque está accediendo al instance
fuera del lock
. Supongamos que omite volatile
y ya ha solucionado el problema de inicialización de esta manera.
public class Foo
{
private static Foo instance;
private static readonly object padlock = new object();
public static Foo GetValue()
{
if (instance == null)
{
lock (padlock)
{
if (instance == null)
{
var temp = new Foo();
temp.Init();
instance = temp;
}
}
}
return instance;
}
private void Init() { /* ... */ }
}
En algún nivel, el compilador de C#, compilador JIT, o hardware podría emitir una secuencia de instrucciones que optimiza la distancia temp
variable y hace que la variable instance
para conseguir asignado antes Init
es RAN. De hecho, podría asignar instance
incluso antes de que se ejecute el constructor. El método Init
hace que el problema sea mucho más fácil de detectar, pero el problema persiste para el constructor.
Esta es una optimización válida ya que las instrucciones se pueden reordenar libremente dentro del bloqueo. Un lock
emite una barrera de memoria, pero solo en las llamadas Monitor.Enter
y Monitor.Exit
.
Ahora, si omite volatile
, el código probablemente aún funcione con la mayoría de las combinaciones de hardware e implementaciones CLI. La razón es porque el hardware x86 tiene un modelo de memoria más ajustado y la implementación de Microsoft del CLR también es bastante estrecha. Sin embargo, la especificación de ECMA sobre este tema es relativamente flexible, lo que significa que otra implementación de la CLI es libre de realizar optimizaciones que Microsoft actualmente elige ignorar. Debe codificar el modelo más débil, que puede ser una fluctuación CLI y no el hardware en el que la mayoría de las personas tiende a concentrarse. Esta es la razón por la que todavía se requiere volatile
.
¿No se puede reordenar solo lectura/escritura con respecto a múltiples hilos ? Comprendí que en un solo subproceso, los efectos secundarios estarían en un orden constante, y que el bloqueo en su lugar evitaría que cualquier otro subproceso observara que algo andaba mal. ¿Estoy fuera de la base aquí también?
Sí. El reordenamiento de instrucciones solo entra en juego cuando más de un subproceso está accediendo a la misma ubicación de memoria. Incluso los modelos más débiles de memoria de software y hardware no permitirán ningún tipo de optimización que cambie el comportamiento de lo que el desarrollador pretendía cuando el código se ejecuta en un hilo. De lo contrario, ningún programa se ejecutará correctamente. El problema está en cómo otros hilos observan lo que está sucediendo en ese hilo. Otros hilos pueden percibir un comportamiento diferente del del hilo de ejecución. Pero, el hilo de ejecución siempre percibe el comportamiento correcto.
No, un lock
por sí solo no evitará que otros subprocesos perciban una secuencia de eventos diferente. La razón es porque el hilo de ejecución podría estar ejecutando las instrucciones dentro del lock
en un orden diferente al previsto por el desarrollador.Solo se crean las barreras de memoria en los puntos de entrada y salida de la cerradura. Por lo tanto, en su ejemplo, la referencia al nuevo objeto podría asignarse a instance
incluso antes de que el constructor se ejecute aunque haya completado esas instrucciones con un lock
.
Usando volatile
, por el contrario, tiene un mayor impacto en cómo el código dentro de los lock
se comporta en comparación con la comprobación inicial de instance
al comienzo del método a pesar de la sabiduría común. Mucha gente piensa que el principal problema es que instance
puede estar añejo sin una lectura volátil. Ese puede ser el caso, pero el problema más grande es que sin una escritura volátil dentro del lock
, otro hilo podría ver instance
en referencia a una instancia para la cual el constructor aún no se ha ejecutado. Una escritura volátil resuelve este problema porque evita que el compilador mueva el código del constructor después de escribir en instance
. Esa es la razón principal por la cual todavía se requiere volatile
.
Tiene razón acerca del modelo de memoria .NET 2.0. No necesitas 'volátil' (ya que casi nunca hace lo que esperarías que hiciera) y un' lock' hace una valla completa. Sin embargo, como señala Chibacity, cuando se trata de seguridad de hilos, es muy fácil pasar por alto una condición de carrera. – Steven