2008-08-13 28 views
63

Me han planteado creer que si varios subprocesos pueden acceder a una variable, todas las lecturas y escrituras en esa variable deben estar protegidas por un código de sincronización, como una instrucción de "bloqueo", porque el procesador podría cambiar a otro subproceso a la mitad de una escritura.¿Está accediendo a una variable en C# una operación atómica?

Sin embargo, yo estaba mirando a través System.Web.Security.Membership usando Reflector y encontré código como este:

public static class Membership 
{ 
    private static bool s_Initialized = false; 
    private static object s_lock = new object(); 
    private static MembershipProvider s_Provider; 

    public static MembershipProvider Provider 
    { 
     get 
     { 
      Initialize(); 
      return s_Provider; 
     } 
    } 

    private static void Initialize() 
    { 
     if (s_Initialized) 
      return; 

     lock(s_lock) 
     { 
      if (s_Initialized) 
       return; 

      // Perform initialization... 
      s_Initialized = true; 
     } 
    } 
} 

Por qué se lee el campo s_Initialized exterior de la cerradura? ¿No podría otro hilo estar tratando de escribir al mismo tiempo? ¿Las lecturas y escrituras de las variables son atómicas?

+0

Para aquellos de ustedes leyendo esta publicación, ya que * actualmente * existe, es un poco confuso. El código anterior se ha ajustado para mostrar una forma ** correcta ** de verificar dos veces con un bloqueo (dentro y fuera). Sin embargo, el texto de la pregunta se refiere a la muestra del código inicial que no incluía la segunda verificación 's_initialized'. Es un poco confuso sin mirar el historial de edición. – GEEF

+0

@GEEF Disculpe la confusión, pero la pregunta SÍ se refiere al código actual, incluido el segundo control dentro del 'bloqueo'. Esta no es una pregunta sobre cómo hacer el bloqueo de doble verificación, sino sobre el acceso a la memoria atómica y si se puede escribir un booleano al mismo tiempo que se lee. –

Respuesta

35

Para la respuesta definitiva, dirigirse a la especificación. :)

Partición I, Sección 12.6.6 de la especificación CLI: "Una CLI conforme garantizará que el acceso de lectura y escritura a ubicaciones de memoria alineadas correctamente no mayores que el tamaño de la palabra nativa es atómico cuando todos los accesos de escritura una ubicación es del mismo tamaño ".

De modo que confirma que s_Initialized nunca será inestable, y que leer y escribir en tipos primitve menores de 32 bits son atómicos.

En particular, double y long (Int64 y UInt64) son no garantiza que sea atómica sobre una plataforma de 32 bits. Puede usar los métodos en la clase Interlocked para protegerlos.

Además, aunque las lecturas y escrituras son atómicas, existe una condición de carrera con tipos de suma, resta e incremento y decremento de primitiva, ya que deben leerse, operarse y reescribirse. La clase interconectada le permite proteger estos usando los métodos CompareExchange y Increment.

El enclavamiento crea una barrera de memoria para evitar que el procesador reordena las lecturas y escrituras. El bloqueo crea la única barrera requerida en este ejemplo.

+3

Aunque el acceso a una ubicación de memoria no mayor que el tamaño de la palabra nativa es atómico, el código de muestra proporcionado en la pregunta no es seguro para subprocesos debido a la reordenación de lectura/escritura. Ver mi respuesta para más detalles. –

+0

Buen lugar, Thomas. Todos deben leer, asegúrese de leer su respuesta también. –

+7

C# 4 especificación '5.5 Atomicidad de las referencias de variables Las lecturas y escrituras de los siguientes tipos de datos son atómicas: bool, char, byte, sbyte, corto, ushort, uint, int, float y tipos de referencia. Además, las lecturas y escrituras de tipos enum con un tipo subyacente en la lista anterior también son atómicas. No se garantiza que las lecturas y escrituras de otros tipos, incluidos long, ulong, double y decimal, así como los tipos definidos por el usuario, sean atómicas. Además de las funciones de la biblioteca diseñadas para tal fin, no hay garantía de lectura, modificación y escritura atómica, como en el caso de incremento o disminución ". – abatishchev

0

Pensé que no estaban seguros del punto del bloqueo en su ejemplo a menos que también le hiciera algo a s_Provider al mismo tiempo, entonces el bloqueo garantizaría que estas llamadas sucedieran juntas.

Hace que //Perform initialization comment cover creating s_Provider? Por ejemplo

private static void Initialize() 
{ 
    if (s_Initialized) 
     return; 

    lock(s_lock) 
    { 
     s_Provider = new MembershipProvider (...) 
     s_Initialized = true; 
    } 
} 

De lo contrario, la propiedad estática-get va a volver a ser nula de todos modos.

+0

El comentario "Realizar inicialización" se usa para todas las configuraciones-lectura, creación de clases y ajustes que establece la membresía para inicializar s_Provider, así que entiendo por qué está en un "candado", por lo que solo se hace una vez. –

2

La función de inicialización es defectuosa. Debe tener un aspecto de la misma familia:

private static void Initialize() 
{ 
    if(s_initialized) 
     return; 

    lock(s_lock) 
    { 
     if(s_Initialized) 
      return; 
     s_Initialized = true; 
    } 
} 

Sin el segundo cheque dentro de la cerradura es posible que el código de inicialización se ejecuta dos veces. El primer control es para que el rendimiento lo salve tomando un bloqueo innecesariamente, y el segundo cheque es para el caso donde un hilo está ejecutando el código de inicialización pero aún no ha establecido el indicador s_Initialized y un segundo hilo pasaría el primer cheque y espera en la cerradura.

+0

Eso no es mejor, también podría eliminar la declaración fuera de la cerradura. Además, si lo único que se hace aquí es establecer inicializado en verdadero, entonces la versión original "no segura". fue lo suficientemente bueno. En el peor de los casos, lo configura dos veces, la única razón para el retorno anticipado es el rendimiento (no la corrección). – Wedge

+1

Dije que el primer cheque es por rendimiento. Las cerraduras son muy caras, así que siempre vale la pena hacerlo. En el segundo punto, creo que es razonable suponer que se ha omitido un código más complejo. De alguna forma, dudo que MS vaya a costar una cerradura innecesariamente. –

+0

Tienes razón. La clase real de Membresía tiene ese segundo control. Lo dejé porque lo que realmente me interesa es si la primera llamada a s_Initialized, fuera del bloqueo, es segura para subprocesos. –

0

Lo que se pregunta es si se accede a un campo en un método varias veces atómico, a lo que la respuesta es no.

En el ejemplo anterior, la rutina de inicialización es defectuosa, ya que puede dar lugar a una inicialización múltiple. Debería verificar el indicador s_Initialized dentro de la cerradura, así como en el exterior, para evitar una condición de carrera en la que varios hilos leen la bandera s_Initialized antes de que ninguno de ellos realmente haga el código de inicialización. Por ejemplo,

private static void Initialize() 
{ 
    if (s_Initialized) 
     return; 

    lock(s_lock) 
    { 
     if (s_Initialized) 
      return; 
     s_Provider = new MembershipProvider (...) 
     s_Initialized = true; 
    } 
} 
1

Creo que está preguntando si s_Initialized podría estar en un estado inestable cuando se lee fuera de la cerradura. La respuesta corta es no. Una asignación/lectura simple se reducirá a una única instrucción de ensamblaje que es atómica en cada procesador que se me ocurre.

No estoy seguro de cuál es el caso para la asignación a variables de 64 bits, depende del procesador, supongo que no es atómico, pero probablemente esté en procesadores modernos de 32 bits y ciertamente en todos los procesadores de 64 bits . La asignación de tipos de valores complejos no será atómica.

1

Las lecturas y escrituras de las variables no son atómicas. Debe usar las API de sincronización para emular las lecturas/escrituras atómicas.

Para obtener una referencia impresionante sobre este y muchos otros problemas relacionados con la simultaneidad, asegúrese de obtener una copia de Joe Duffy's latest spectacle. Es un destripador!

1

"¿Está accediendo a una variable en C# una operación atómica?"

Nope. Y no es algo de C#, ni siquiera es algo de .net, es algo de procesador.

OJ tiene la certeza de que Joe Duffy es el tipo al que debe acudir para obtener este tipo de información. Y "interbloqueado" es un excelente término de búsqueda para usar si quiere saber más.

Las "lecturas rasgadas" pueden ocurrir en cualquier valor cuyos campos suman más que el tamaño de un puntero.

-1

Ack, nevermind ... como se señaló, esto es de hecho incorrecto. No evita que un segundo hilo ingrese a la sección de código "inicializar". Bah.

También puede decorar s_Initialized con la palabra clave volátil y renunciar al uso de bloqueo por completo.

+0

Hacer s_Initialized volátil no ayudará. En particular, no impedirá que dos CPU lean el mismo valor y procedan erróneamente a crear el objeto. Hacer que sea volátil podría * parecer * corregir problemas, porque los accesos a variables volátiles pueden serializar los accesos de memoria por completo, dejando la memoria en un estado inconsistente periódicamente, pero ocultando los errores lo suficientemente bien para pasar las pruebas pero fallando bajo el estrés del mundo real. – doug65536

1

También puede decorar s_Initialized con la palabra clave volátil y renunciar al uso de bloqueo por completo.

Eso no es correcto. Todavía se encontrará con el problema de que un segundo hilo pase la verificación antes de que el primer hilo haya tenido la oportunidad de establecer el indicador, lo que dará como resultado múltiples ejecuciones del código de inicialización.

7

La respuesta correcta parece ser "Sí, principalmente".

  1. La respuesta de John que hace referencia a la especificación CLI indica que los accesos a variables que no superan los 32 bits en un procesador de 32 bits son atómicos.
  2. La confirmación adicional de la especificación C#, apartado 5.5, Atomicity of variable references:

    Lee y escribe de los siguientes tipos de datos son atómica: bool, char, byte, sbyte, corta, ushort, uint, int, float y tipos de referencia. Además, las lecturas y escrituras de tipos enum con un tipo subyacente en la lista anterior también son atómicas. No se garantiza que las lecturas y escrituras de otros tipos, incluidos long, ulong, double y decimal, así como los tipos definidos por el usuario, sean atómicas.

  3. El código en mi ejemplo fue parafraseado de la clase de miembro, según lo escrito por el equipo de ASP.NET a sí mismos, por lo que siempre era seguro asumir que la forma en que se accede al campo s_Initialized es correcta. Ahora sabemos por qué.

Edit: Como Thomas Danecker señala, a pesar de que el acceso del campo es atómica, s_Initialized realmente debería estar marcado volátil para asegurarse de que el bloqueo no se rompe por el procesador reordenar la lectura y escribe

+0

No es correcto. Vea mi respuesta por el motivo. –

+0

Veo su punto, pero el código en mi ejemplo es exacto con respecto a cómo se realiza el bloqueo en la clase de Membresía real. Quizás el equipo de ASP.NET sepa lo suficiente sobre su compilador y su procesador de destino para estar seguro de que no se realizará ningún cambio de orden. De todos modos ... –

+0

... Estoy de acuerdo con Joe Duffy en el artículo al que se vincula cuando dice que siempre debemos usar "volátil" en situaciones como esta solo para estar seguros. –

11

Espera, la pregunta que está en el título definitivamente no es la verdadera pregunta que Rory está haciendo.

La pregunta titular tiene la respuesta simple de "No", pero esto no es de ninguna ayuda cuando ve la pregunta real, que no creo que nadie haya dado una respuesta simple.

La verdadera pregunta que Rory hace se presenta mucho más tarde y es más pertinente al ejemplo que da.

¿Por qué el campo s_Initialized lee fuera de la cerradura?

La respuesta a esto también es simple, aunque no está relacionada con la atomicidad del acceso variable.

El campo s_Initialized se lee fuera de la cerradura porque las cerraduras son caras.

Dado que el campo s_Initialized es esencialmente "escribir una vez", nunca devolverá un falso positivo.

Es económico leerlo fuera de la cerradura.

Esta es una actividadbajo costo con un alto probabilidad de tener un beneficio .

Es por eso que se lee fuera de la cerradura para evitar pagar el costo de usar una cerradura a menos que esté indicado.

Si las cerraduras fueran baratas, el código sería más simple, y omitir ese primer control.

(editar: buena respuesta de rory sigue. Yeh, las lecturas booleanas son muy atómicas. Si alguien construyera un procesador con lecturas booleanas no atómicas, aparecerían en el DailyWTF.)

1

@Leon
veo su punto - la forma en que he pedido, y luego comentado, la cuestión permite que sea tomada en un par de maneras diferentes.

Para que quede claro, yo quería saber si era seguro tener hilos concurrentes leer y escribir en un campo booleano sin ningún código de sincronización explícita, es decir, está accediendo a un valor lógico (o de otro tipo primitivo mecanografiado) Variable atómica.

Luego utilicé el código de membresía para dar un ejemplo concreto, pero eso introdujo un montón de distracciones, como el bloqueo de doble verificación, el hecho de que s_Initialized solo se establece una vez, y comenté el código de inicialización mismo .

Mi mal.

34

Esta es una forma (mala) del patrón de bloqueo de doble verificación que es no hilo seguro en C#!

Hay un gran problema en este código:

s_Initialized no es volátil. Esto significa que las escrituras en el código de inicialización pueden moverse después de que s_Initialized se establece en verdadero y otros hilos pueden ver el código no inicializado, incluso si s_Initialized es verdadero para ellos. Esto no se aplica a la implementación de Microsoft del Marco porque cada escritura es una escritura volátil.

Pero también en la implementación de Microsoft, lee los datos sin inicializar se pueden reordenar (es decir precapturados por la CPU), por lo que si s_Initialized es cierto, la lectura de los datos que deben ser inicializado puede dar lugar a la lectura de los datos antiguos, sin inicializar a causa de caché -hits (es decir, las lecturas se reordenan).

Por ejemplo:

Thread 1 reads s_Provider (which is null) 
Thread 2 initializes the data 
Thread 2 sets s\_Initialized to true 
Thread 1 reads s\_Initialized (which is true now) 
Thread 1 uses the previously read Provider and gets a NullReferenceException 

Traslado de la lectura de s_Provider antes de la lectura de s_Initialized es perfectamente legal porque no hay lectura volátil en cualquier lugar.

Si s_Initialized sería volátil, la lectura de s_Provider no se podría mover antes de la lectura de s_Initialized y también la inicialización del proveedor no se puede mover después de s_Initialized se establece en true y todo está bien ahora.

Joe Duffy también escribió un artículo acerca de este problema: Broken variants on double-checked locking

+0

Buen punto. Leer el artículo al que vinculó, si el código en Membresía es correcto o incorrecto, parece depender de los detalles precisos del compilador y el procesador, pero estoy de acuerdo en que s_Initialized debería volverse volátil solo para estar seguro. –

+0

It * is * wrong según el Modo de memoria de .net. En ciertos procesadores y/o con ciertos compiladores, puede tener la suerte de que todavía funciona. Joe Duffy escribió otro artículo: http://www.bluebytesoftware.com/blog/2008/07/17/LoadsCannotPassOtherLoadsIsAMyth.aspx –

+0

La ausencia de reordenación de escritura es una característica de x86 y x64 pero no necesariamente de Microsoft .NET, es decir, no confiaría en él si Microsoft agrega soporte para otras arquitecturas donde el hardware reordena las escrituras. –

0

Para que el código siempre funciona en arquitecturas débilmente ordenado, debe poner un MemoryBarrier antes de escribir s_Initialized.

s_Provider = new MemershipProvider; 

// MUST PUT BARRIER HERE to make sure the memory writes from the assignment 
// and the constructor have been wriitten to memory 
// BEFORE the write to s_Initialized! 
Thread.MemoryBarrier(); 

// Now that we've guaranteed that the writes above 
// will be globally first, set the flag 
s_Initialized = true; 

La memoria escribe que suceda en el constructor MembershipProvider y la escritura en s_Provider No se garantiza que sucederá antes de grabar en s_Initialized en un procesador débilmente ordenado.

Muchos de los pensamientos en este hilo se trata de si algo es atómico o no. Ese no es el problema. El problema es para que las escrituras de su subproceso sean visibles para otros subprocesos. En arquitecturas ordenadas débilmente, las escrituras en la memoria no ocurren en orden y ESO es el problema real, no si una variable encaja dentro del bus de datos.

EDIT: En realidad, estoy mezclando plataformas en mis declaraciones.En C#, la especificación de CLR requiere que las escrituras sean visibles a nivel mundial, en orden (usando costosas instrucciones de tienda para cada tienda si es necesario). Por lo tanto, no necesita tener esa barrera de memoria allí. Sin embargo, si fuera C o C++ donde no existe tal garantía de orden de visibilidad global, y su plataforma objetivo puede tener memoria débilmente ordenada, y es multiproceso, entonces debería asegurarse de que las escrituras de los constructores estén visibles globalmente antes de actualizar s_Initialized , que se prueba fuera de la cerradura.

0

Un If (itisso) { comprobar en un booleano es atómico, pero incluso si no fuera no hay necesidad de bloquear el primer cheque.

Si un hilo ha completado la inicialización, entonces será verdadero. No importa si varios hilos están revisando a la vez. Todos obtendrán la misma respuesta y no habrá conflicto.

La segunda comprobación dentro del bloqueo es necesaria porque es posible que otro hilo haya agarrado primero el bloqueo y haya completado el proceso de inicialización.

Cuestiones relacionadas