2011-08-26 14 views
10

Recientemente encontré the following post on the Resharper website. Fue una discusión de bloqueo doble comprobación, y tenía el siguiente código:Garantías del modelo de la memoria en la cerradura doble-comprobada

public class Foo 
{ 
    private static volatile Foo instance; 
    private static readonly object padlock = new object(); 

    public static Foo GetValue() 
    { 
     if (instance == null) 
     { 
      lock (padlock) 
      { 
       if (instance == null) 
       { 
        instance = new Foo(); 
        instance.Init(); 
       } 
      } 
     } 
     return instance; 
    } 

    private void Init() 
    { 
     ... 
    } 
} 

El puesto luego hace la afirmación de que

Si asumimos que Init() es un método utilizado para inicializar el estado de Foo, entonces el código anterior puede no funcionar como se esperaba debido al modelo de memoria que no garantiza el orden de las lecturas y escrituras. Como resultado de , la llamada a Init() puede ocurrir realmente antes de que la instancia de la variable esté en un estado consistente.

Aquí están mis preguntas:

  1. tenía entendido que el modelo de memoria de .NET (por lo menos desde 2.0) tiene no requiere que instance ser declarado como volatile, ya que proporcionaría un lock valla de memoria completa. ¿No es ese el caso, o estaba mal informado?

  2. ¿El reordenamiento de lectura/escritura no es solo observable con respecto a múltiples hilos? Comprendí que en un solo subproceso, los efectos secundarios estarían en un orden constante, y que el lock en su lugar evitaría que cualquier otro subproceso observara que algo andaba mal. ¿Estoy fuera de la base aquí también?

+2

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

Respuesta

18

El gran problema con el ejemplo es que el primer cheque nulo no está bloqueada, por lo instancia no puede ser nula, pero antes de Init ha sido llamado. Esto puede provocar que los subprocesos usen una instancia antes de que se haya invocado Init.

La versión correcta debe ser, por lo tanto:

public static Foo GetValue() 
{ 
    if (instance == null) 
    { 
     lock (padlock) 
     { 
      if (instance == null) 
      { 
       var foo = new Foo(); 
       foo.Init(); 
       instance = foo; 
      } 
     } 
    } 

    return instance; 
} 
+2

Eso es muy nítido. Me lo perdí a mí mismo. No me está agregando la versión correcta de ese código a su respuesta para completar, ¿verdad? – Steven

+2

@Steven Correcto. Saludos por la edición - apreciado. ¡Muy difícil desde mi teléfono! :) –

+3

Debería obtener puntos extra por responder a este móvil :-) – Steven

1

Si leo el código correctamente, la cuestión es:

de llamadas 1 comienza el método, se encuentra instancia == null para ser verdad, entra en el lock, la instancia de STILL es nula y crea una instancia.

Antes de llamar a Init(), el hilo de la persona que llama 1 se suspende y la persona que llama 2 ingresa al método. La persona que llama 2 encuentra que la instancia NO es nula, y continúa usándola antes de que la persona que llama 1 pueda inicializarla.

0

Por un lado se crea un "cerco completo" pero lo que la cita se refiere es lo que pasa "dentro de la cerca" de un "caso de bloqueo doble comprobación" ... ver una explicación http://blogs.msdn.com/b/cbrumme/archive/2003/05/17/51445.aspx

afirma:

However, we have to assume that a series of stores have taken place during construction 
of ‘a’. Those stores can be arbitrarily reordered, including the possibility of delaying 
them until after the publishing store which assigns the new object to ‘a’. At that point, 
there is a small window before the store.release implied by leaving the lock. Inside that 
window, other CPUs can navigate through the reference ‘a’ and see a partially constructed 
instance. 

Reemplazar a en la frase anterior con instance de usted ... ejemplo

Además comprobar esta http://blogs.msdn.com/b/brada/archive/2004/05/12/130935.aspx - es explica lo que volatile logra en su escenario ...

Una buena explicación de vallas y volatile y cómo volatile tiene incluso efectos diferentes dependiendo del procesador que ejecuta el código en Ver http://www.albahari.com/threading/part4.aspx y aún más/mejor información vea http://csharpindepth.com/Articles/General/Singleton.aspx

12

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.

+1

Excelente respuesta. –

Cuestiones relacionadas