2011-05-28 16 views
13

Si entiendo el significado de volátil y MemoryBarrier correctamente que el programa siguiente nunca debe poder mostrar ningún resultado.¿Por qué los volátiles y MemoryBarrier no impiden el reordenamiento de operaciones?

Captura el reordenamiento de las operaciones de escritura cada vez que lo ejecuto. No importa si lo ejecuto en Depurar o Liberar. Tampoco importa si lo ejecuto como aplicación de 32 bits o 64 bits.

¿Por qué sucede?

using System; 
    using System.Threading; 
    using System.Threading.Tasks; 

    namespace FlipFlop 
    { 
     class Program 
     { 
      //Declaring these variables as volatile should instruct compiler to 
      //flush all caches from registers into the memory. 
      static volatile int a; 
      static volatile int b; 

      //Track a number of iteration that it took to detect operation reordering. 
      static long iterations = 0; 

      static object locker = new object(); 

      //Indicates that operation reordering is not found yet. 
      static volatile bool continueTrying = true; 

      //Indicates that Check method should continue. 
      static volatile bool continueChecking = true; 

      static void Main(string[] args) 
      { 
       //Restarting test until able to catch reordering. 
       while (continueTrying) 
       { 
        iterations++; 
        var checker = new Task(Check); 
        var writter = new Task(Write); 
        lock (locker) 
        { 
         continueChecking = true; 
         checker.Start(); 

        } 
        writter.Start(); 
        checker.Wait(); 
        writter.Wait(); 
       } 
       Console.ReadKey(); 
      } 

      static void Write() 
      { 
       //Writing is locked until Main will start Check() method. 
       lock (locker) 
       { 
        //Using memory barrier should prevent opration reordering. 
        a = 1; 
        Thread.MemoryBarrier(); 
        b = 10; 
        Thread.MemoryBarrier(); 
        b = 20; 
        Thread.MemoryBarrier(); 
        a = 2; 

        //Stops spinning in the Check method. 
        continueChecking = false; 
       } 
      } 

      static void Check() 
      { 
       //Spins until finds operation reordering or stopped by Write method. 
       while (continueChecking) 
       { 
        int tempA = a; 
        int tempB = b; 

        if (tempB == 10 && tempA == 2) 
        { 
         continueTrying = false; 
         Console.WriteLine("Caught when a = {0} and b = {1}", tempA, tempB); 
         Console.WriteLine("In " + iterations + " iterations."); 
         break; 
        } 
       } 
      } 
     } 
    } 
+0

b/c este es su propia idea. Las barreras de memoria (escritura) solo aseguran que todas las operaciones hasta el momento se limpian, por lo tanto, las siguientes son parte de la barrera. – bestsss

+0

Lo más interesante de su código es que eliminar todas las líneas 'Thread.MemoryBarrier();' arregla su problema =) – Mikant

+1

@Mikant: No, eso no soluciona el problema. Simplemente lo hace muy poco probable. Déjalo funcionar durante unos días y aún podría suceder. –

Respuesta

3

No creo que esto esté reordenando.

Esta pieza de código es simplemente no seguro para subprocesos:

while (continueChecking) 
{ 
    int tempA = a; 
    int tempB = b; 
    ... 

Creo que este escenario es posible:

  1. int tempA = a; ejecuta con los valores de la última bucle (a == 2)
  2. Hay un cambio de contexto a la rosca Write
  3. b = 10 y el bucle se detiene
  4. Hay un cambio de contexto para la comprobación de rosca
  5. int tempB = b; ejecuta con b == 10

Me he dado cuenta de que las llamadas a MemoryBarrier() mejoran las posibilidades de este escenario. Probablemente porque causan más cambio de contexto.

+0

Gracias. Estás en lo correcto. – Dennis

+0

¿Dudaste y borraste? –

+0

@Marc: Tuve la respuesta/visión correcta, pero en la segunda lectura me confundí con un error tipográfico mío:}. –

0

El resultado no tiene nada que ver con la reordenación, con las barries de memoria o con volátiles. Todos estos constructos son necesarios para evitar los efectos del compilador o el reordenamiento de la CPU de las instrucciones.

Pero este programa produciría el mismo resultado incluso asumiendo un modelo de memoria de CPU única y consistente y sin optimización del compilador.

Antes que nada, observe que habrá múltiples tareas Write() comenzadas en paralelo. Se ejecutan secuencialmente debido a lock() dentro de Write(), pero un método signle Check() puede leer a y b producido por diferentes instancias de tareas Write().

Debido Check() función no tiene sincronización con Write función - se puede leer a y b en dos momentos diferentes y arbitrarias. No hay nada en su código que impida que Check() lea a producido por Write() anterior en un momento y luego lea b producido por siguiente Write() en otro momento. Lo primero de todo es que necesita sincronización (bloqueo) en Check() y luego podría (pero probablemente no en este caso) necesita barreras de memoria y volátiles para luchar contra los problemas del modelo de memoria.

Esto es todo lo que necesita:

 int tempA, tempB; 
     lock (locker) 
     { 
      tempA = a; 
      tempB = b; 
     } 
+2

Parece un poco más interesante que eso. Si no hay reordenamiento, ¿qué escenario daría esos valores para tempA/tempB? Tenga en cuenta que solo hay una Escritura por prueba –

+0

(la cerradura solo está destinada a retrasar el acceso para que la escritura no suceda demasiado pronto; como ocurre, no necesariamente hace esto, ya que puede haber un retraso entre el inicio y el inicio real - pero se acerca lo suficiente, parece) –

+0

@Marc - el comprobador se inicia antes que Writer, por lo que tiene la posibilidad de observar todas las escrituras que Writer hace.MemoryBarrier en Writer solo empeora las cosas, ya que aumenta el cambio para que Checker vea todos los valores intermedios – Michael

-1
  1. Si utiliza MemoryBarrier en writer, ¿por qué no se hace eso en checker? Ponga Thread.MemoryBarrier(); antes de int tempA = a;.

  2. Llamar a Thread.MemoryBarrier(); tantas veces bloquea todas las ventajas del método. Llámalo solo una vez antes o después de a = 1;.

+0

Esto realmente no explica lo que está pasando. ¿Cómo corrigen estas sugerencias el problema en términos del modelo de memoria .NET? – dtb

+0

@dtb estaba un poco más claro antes de editar mi publicación y borré una línea de que mi publicación podría ser una pista para @Dennis ... no hay nada misterioso sucediendo en su código. y no hay problemas con el modelo de memoria .NET. todo funciona como está escrito. entonces creo que Dennis es capaz de obtener una respuesta a la pregunta que sigue a mis escritos. – Mikant

+1

Quizás Dennis pueda obtener la respuesta siguiendo tus pistas, pero ¿por qué simplemente no das la respuesta directamente para que todos la vean? – dtb

10

No están limpiando las variables entre las pruebas, por lo que (para todos excepto el primero) es inicialmente a2 y b es 20 - antesWrite ha hecho nada .

Check puede conseguir que valor inicial de a (así tempA es 2), y luego Write puede conseguir en, llegar tan lejos como cambiar b-10.

Ahora Check lee b (entonces tempB es 10).

Et voila. No es necesario reordenar para repro.

Restablecer a y b a 0 entre ejecuciones y espero que desaparecerá.

corregir: confirmado; "como está" obtengo el problema casi de inmediato (< 2000 iteraciones); pero al agregar:

while (continueTrying) 
{ 
    a = b = 0; // reset <======= added this 

, luego se repite sin importar la cantidad de tiempo.

o como un flujo:

Write     A= B=  Check 

(except first run)  2 20 
             int tempA = a; 
a = 1;     1 20 
Thread.MemoryBarrier(); 
b = 10;     1 10 
             int tempB = b; 
+0

También está en lo cierto, pero Henk Holterman fue el primero. – Dennis

+0

@Dennis no hay problema; Creo que borró y luego desapareció, por lo que no estaba allí. Aceptar lo suyo es lo correcto para hacer –

Cuestiones relacionadas