2010-08-24 33 views
41

En "C# 4 en una cáscara de nuez", el autor muestra que esta clase puede escribir 0 veces sin MemoryBarrier, aunque no puedo reproducir en mi Core2Duo:¿Por qué necesitamos Thread.MemoryBarrier()?

public class Foo 
{ 
    int _answer; 
    bool _complete; 
    public void A() 
    { 
     _answer = 123; 
     //Thread.MemoryBarrier(); // Barrier 1 
     _complete = true; 
     //Thread.MemoryBarrier(); // Barrier 2 
    } 
    public void B() 
    { 
     //Thread.MemoryBarrier(); // Barrier 3 
     if (_complete) 
     { 
      //Thread.MemoryBarrier();  // Barrier 4 
      Console.WriteLine(_answer); 
     } 
    } 
} 

private static void ThreadInverteOrdemComandos() 
{ 
    Foo obj = new Foo(); 

    Task.Factory.StartNew(obj.A); 
    Task.Factory.StartNew(obj.B); 

    Thread.Sleep(10); 
} 

Esta necesidad parece una locura para mí. ¿Cómo puedo reconocer todos los casos posibles que esto puede ocurrir? Creo que si el procesador cambia el orden de las operaciones, debe garantizar que el comportamiento no cambie.

¿Se molesta en usar Barriers?

Respuesta

62

Va a ser muy difícil reproducir este error. De hecho, iría tan lejos como para decir que nunca serás capaz de reproducirlo usando .NET Framework. La razón es porque la implementación de Microsoft usa un fuerte modelo de memoria para escrituras. Eso significa que las escrituras son tratadas como si fueran volátiles. Una escritura volátil tiene semántica de liberación de bloqueo, lo que significa que todas las escrituras previas deben confirmarse antes de la escritura actual.

Sin embargo, la especificación ECMA tiene un modelo de memoria más débil. Por lo tanto, es teóricamente posible que Mono o incluso una versión futura de .NET Framework comience a exhibir el comportamiento erróneo.

Entonces, lo que estoy diciendo es que es muy poco probable que eliminar las barreras # 1 y # 2 tenga un impacto en el comportamiento del programa. Eso, por supuesto, no es una garantía, sino una observación basada en la implementación actual del CLR solamente.

La eliminación de las barreras n. ° 3 y n. ° 4 definitivamente tendrá un impacto. Esto es bastante fácil de reproducir. Bueno, no este ejemplo per se, pero el siguiente código es una de las demostraciones más conocidas. Tiene que ser compilado usando la compilación Release y se ejecutó fuera del depurador. El error es que el programa no termina. Puede solucionar el error haciendo una llamada al Thread.MemoryBarrier dentro del lazo while o marcando stop como volatile.

class Program 
{ 
    static bool stop = false; 

    public static void Main(string[] args) 
    { 
     var t = new Thread(() => 
     { 
      Console.WriteLine("thread begin"); 
      bool toggle = false; 
      while (!stop) 
      { 
       toggle = !toggle; 
      } 
      Console.WriteLine("thread end"); 
     }); 
     t.Start(); 
     Thread.Sleep(1000); 
     stop = true; 
     Console.WriteLine("stop = true"); 
     Console.WriteLine("waiting..."); 
     t.Join(); 
    } 
} 

La razón por la cual algunos errores de roscado son difíciles de reproducir se debe a que las mismas tácticas que utilizan para simular el entrelazado de hilo en realidad puede corregir el error. Thread.Sleep es el ejemplo más notable porque genera barreras de memoria. Puede verificarlo haciendo una llamada dentro del bucle while y observando que el error desaparece.

Puede ver mi respuesta here para otro análisis del ejemplo del libro que ha citado.

+0

En el libro también tiene este ejemplo. Podría probar y pasar. –

+0

No repro en vs2015 hasta el momento. – AgentFire

+3

@Brian Gideon Puede ser que sea muy tonto, pero no entiendo por qué el programa de tu ejemplo nunca termina. ¿Por qué 'while (! Stop)' nunca obtener 'false'? ¿Puedes explicar un poco más? ¿O puede sugerir algún blog detallado? Leí el artículo OP publicado (ejemplo de albahari), me confundí también allí; para mí, la barrera 1 es suficiente, ¿por qué hay otras barreras? MSDN dice que 'MemoryBarrier' impide el reordenamiento de las instrucciones. Entonces, ¿por qué solo la barrera 1 no es suficiente (porque 'completed' no se puede ejecutar antes de que se establezca' answer')? Realmente no lo entiendo. – mshsayem

2

Si usa volatile y lock, la barrera de memoria está incorporada. Pero, sí, la necesita de lo contrario. Habiendo dicho eso, sospecho que necesitas la mitad de lo que muestra tu ejemplo.

+0

Sí, el autor dice que Barriers 2 e 3 es solo para garantizar que si A se ejecuta antes que B, B ingrese el if –

+0

Todos son necesarios de forma errótica si se desea una estrategia sin bloqueo. –

+1

@Brian: 1 y 4 son necesarios. No estoy tan seguro de 2 y 3. Parece que el peor caso es que pierde una condición de carrera que podría haber ganado o no. –

2

Es muy difícil de reproducir errores de subprocesos múltiples: por lo general, debe ejecutar el código de prueba muchas veces (miles) y tener algún control automático que marcará si se produce el error. Puede intentar agregar un Thread.Sleep (10) corto entre algunas de las líneas, pero nuevamente no siempre garantiza que obtendrá los mismos problemas que sin él.

Las barreras de memoria se introdujeron para las personas que necesitan hacer una optimización del rendimiento de bajo nivel muy difícil de su código multiproceso. En la mayoría de los casos, estará mejor cuando utilice otras primitivas de sincronización, es decir, volátiles o de bloqueo.

+9

El problema con 'Thread.Sleep' es que puede generar una barrera de memoria. Entonces, usar ese mecanismo para tratar de reproducir errores de enhebrado puede arreglar el error. –

+0

interesante ... – Azodious

0

Si alguna vez toca datos de dos subprocesos diferentes, esto puede ocurrir. Este es uno de los trucos que los procesadores usan para aumentar la velocidad: podrías construir procesadores que no hicieran esto, pero serían mucho más lentos, por lo que ya nadie lo hace. Probablemente deberías leer algo como Hennessey and Patterson para reconocer todos los diversos tipos de condiciones de carrera.

Siempre uso algún tipo de herramienta de nivel superior, como un monitor o un candado, pero internamente están haciendo algo similar o están implementados con barreras.

10

Las probabilidades son muy bueno que la primera tarea se completa cuando la segunda tarea incluso comienza a ejecutarse. Solo puede observar este comportamiento si ambos subprocesos ejecutan ese código simultáneamente y no hay operaciones intermedias de sincronización de caché. Hay uno en su código, el método StartNew() tendrá un bloqueo dentro del administrador de grupo de subprocesos en alguna parte.

Obtener dos hilos para ejecutar este código simultáneamente es muy difícil. Este código se completa en un par de nanosegundos. Tendría que probar miles de millones de veces e introducir retrasos variables para tener probabilidades. No hay mucho que apuntar a esto, por supuesto, el verdadero problema es cuando esto ocurre al azar cuando no lo espera.

Aléjate de esto, utiliza la instrucción de bloqueo para escribir un código de varios subprocesos.

+0

El problema es que no necesito bloqueo aquí. El método B nunca debe escribir 0, ya que _complete solo se convierte en verdadero después de configurar _answer. Pero es solo un ejemplo. –

+1

@Fujiy: Si elimina la barrera 1, entonces el compilador/jitter/procesador podría reordenar 'A' para que' _completo' se establezca en 'true' * antes de que' '_answer' se establezca en' 123'. es decir, 'B' podría ver que' _complete' es 'true' y luego leer' 0' de la respuesta. – LukeH

+0

@Fujiy: Del mismo modo, si elimina la barrera 4: el compilador/jitter/procesador podría reordenar 'B' para que' _answer' sea leído * antes * '_completo'. es decir, 'B' podría leer' 0' de la respuesta mientras '_completo' es' falso' y luego leer 'true' de' _complete' * después de que * se haya actualizado. – LukeH

1

Voy a citar a uno de los grandes artículos sobre multi-threading:

Consideremos el siguiente ejemplo:

class Foo 
{ 
    int _answer; 
    bool _complete; 

    void A() 
    { 
    _answer = 123; 
    _complete = true; 
    } 

    void B() 
    { 
    if (_complete) Console.WriteLine (_answer); 
    } 
} 

Si los métodos A y B corrieron simultáneamente en diferentes hilos, ¿podría ser posible que B escriba "0"? La respuesta es sí, por las siguientes razones :

El compilador, CLR o CPU pueden reordenar las instrucciones de su programa a para mejorar la eficiencia. El compilador, el CLR o la CPU pueden introducir optimizaciones de almacenamiento en memoria caché de modo que las asignaciones a las variables no sean visibles para otros hilos de inmediato. C# y el tiempo de ejecución son muy cuidadosos al asegurarse de que tales optimizaciones no rompan el código ordinario de un solo subproceso - o código multiproceso que hace un uso adecuado de los bloqueos. Fuera de de estos escenarios, debe derrotar explícitamente estas optimizaciones por creando barreras de memoria (también llamadas vallas de memoria) para limitar los efectos de reordenamiento de instrucciones y caché de lectura/escritura.

vallas completas

El tipo más simple de barrera de la memoria es una memoria llena barrera (completa valla) que impide cualquier tipo de instrucción reordenación o caché de alrededor de esa valla. Calling Thread.MemoryBarrier genera una valla completa ; podemos fijar nuestro ejemplo mediante la aplicación de cuatro vallas completos como sigue:

class Foo 
{ 
    int _answer; 
    bool _complete; 

    void A() 
    { 
    _answer = 123; 
    Thread.MemoryBarrier(); // Barrier 1 
    _complete = true; 
    Thread.MemoryBarrier(); // Barrier 2 
    } 

    void B() 
    { 
    Thread.MemoryBarrier(); // Barrier 3 
    if (_complete) 
    { 
     Thread.MemoryBarrier();  // Barrier 4 
     Console.WriteLine (_answer); 
    } 
    } 
} 

Toda la teoría detrás de Thread.MemoryBarrier y por qué tenemos que utilizar en escenarios de no bloqueo para hacer el código seguro y robusto se describe muy bien aquí: http://www.albahari.com/threading/part4.aspx