2012-05-01 7 views
6

En su excelente tratado sobre enhebrado en C#, Joseph Albahari propuso el siguiente programa simple para demostrar por qué tenemos que usar algún tipo de valla de memoria alrededor de datos que se leen y escriben trapos. El programa nunca termina si se compila en modo de lanzamiento y de funcionamiento libre sin depurador:variable compartida entre dos subprocesos se comporta de manera diferente a la propiedad compartida

static void Main() 
    { 
    bool complete = false; 
    var t = new Thread(() => 
    { 
     bool toggle = false; 
     while (!complete) toggle = !toggle; 
    }); 
    t.Start(); 
    Thread.Sleep(1000); 
    complete = true;     
    t.Join(); // Blocks indefinitely 
    } 

Mi pregunta es, ¿por qué la siguiente versión ligeramente modificada del programa anterior ya no bloquee indefinidamente ??

class Foo 
{ 
    public bool Complete { get; set; } 
} 

class Program 
{ 
    static void Main() 
    { 
    var foo = new Foo(); 
    var t = new Thread(() => 
    { 
     bool toggle = false; 
     while (!foo.Complete) toggle = !toggle; 
    }); 
    t.Start(); 
    Thread.Sleep(1000); 
    foo.Complete = true;     
    t.Join(); // No longer blocks indefinitely!!! 
    } 
} 

Considerando que el siguiente sigue impidiendo indefinidamente:

class Foo 
{ 
    public bool Complete;// { get; set; } 
} 

class Program 
{ 
    static void Main() 
    { 
    var foo = new Foo(); 
    var t = new Thread(() => 
    { 
     bool toggle = false; 
     while (!foo.Complete) toggle = !toggle; 
    }); 
    t.Start(); 
    Thread.Sleep(1000); 
    foo.Complete = true;     
    t.Join(); // Still blocks indefinitely!!! 
    } 
} 

como lo hace el siguiente:

class Program 
{ 
    static bool Complete { get; set; } 

    static void Main() 
    { 
    var t = new Thread(() => 
    { 
     bool toggle = false; 
     while (!Complete) toggle = !toggle; 
    }); 
    t.Start(); 
    Thread.Sleep(1000); 
    Complete = true;     
    t.Join(); // Still blocks indefinitely!!! 
    } 
} 
+0

El título de su pregunta es más amplio de lo que necesita ser para cubrir el material en cuestión. No todos los códigos son tan simples como esto. –

+0

¿Has comparado el IL de ambos programas? – Oded

+0

sí comparé el IL, pero realmente no vi nada que pudiera darme una pista – dmg

Respuesta

7

En el primer ejemplo Complete es una variable miembro y podría almacenarse en caché para cada subproceso. Como no está utilizando el bloqueo, las actualizaciones de esa variable no se pueden descargar a la memoria principal y el otro subproceso verá un valor obsoleto para esa variable.

En el segundo ejemplo, donde Complete es una propiedad, en realidad está llamando a una función en el objeto Foo para devolver un valor. Creo que si bien las variables simples pueden almacenarse en caché en los registros, es posible que el compilador no siempre optimice las propiedades reales de esa manera.

EDIT:

En cuanto a la optimización de las propiedades automáticas - Creo que no hay nada garantizado por la especificación en ese sentido. Básicamente depende de si el compilador/tiempo de ejecución podrá o no optimizar el getter/setter.

En el caso en que está en el mismo objeto, parece que sí. En el otro caso, parece que no. De cualquier manera, no apostaría en eso. La forma más fácil de resolver esto sería usar una variable miembro simple y la marca es como volotile para garantizar que siempre esté sincronizada con la memoria principal.

+0

¿Qué tal el último ejemplo que acabo de agregar? – dmg

+0

@dmg - editó mi respuesta. Dado que la especificación no ofrece ninguna garantía al respecto, todo se reduce a apostar sobre cómo el compilador puede o no optimizar las propiedades automáticas. –

+0

parece que eso es lo que está pasando. si la propiedad Complete pertenece a esta clase, se optimiza a la distancia, pero si pertenece a una clase diferente, entonces no lo es. – dmg

5

Esto se debe a que en el primer fragmento de código que ya ha proporcionado, que hizo una expresión lambda que se cerró sobre el valor booleano complete - entonces, cuando el compilador reescribe eso, captura una copia del valor, no una referencia. Del mismo modo, en el segundo caso, está capturando una referencia en lugar de una copia, debido al cierre del objeto Foo, y por lo tanto, cuando cambia el valor subyacente, el cambio se nota debido a la referencia.

+0

¿Puedes explicar cómo se captura el valor "completo"? Esperaría que se capturara por referencia, ya que esto es lo que generalmente ocurre en una expresión lambda. –

+1

'bool' es un tipo de datos de valor, por lo que es imposible de capturar por referencia. – Tejs

+0

Acabo de agregar otro fragmento de código. ¿El compilador optimiza el campo de miembro público? Complete de la misma manera que lo hace la variable bool local, pero no puede hacer la misma optimización si el campo miembro público se reemplaza con una propiedad? – dmg

3

Las otras respuestas explican lo que sucede en términos técnicamente correctos. Déjame ver si puedo explicarlo en inglés.

El primer ejemplo dice "Loop hasta que esta ubicación variable sea verdadera". El nuevo subproceso crea una copia de esa ubicación de variable (porque es un tipo de valor) y continúa en bucle para siempre. Si la variable hubiera sido un tipo de referencia, habría hecho una copia de la referencia, pero dado que la referencia apuntaba a la misma ubicación de memoria, habría funcionado.

El segundo ejemplo dice "Bucle hasta que este método (el getter) devuelva verdadero". El nuevo hilo no puede crear una copia de un método, por lo que crea una copia de la referencia a la instancia de la clase en cuestión, y llama repetidamente el captador en esa instancia hasta que devuelve verdadero (leyendo repetidamente la misma ubicación variable que se establece verdadero en el hilo principal).

El tercer ejemplo es el mismo que el primero. El hecho de que la variable cerrada sea miembro de otra instancia de clase no es relevante.

+0

Supongo que, en el cuarto ejemplo, el compilador optimiza la llamada a la propiedad de obtención estática y la trata como si fuera una copia de una variable. – dmg

+0

En el cuarto ejemplo (lo siento, no lo había visto hasta ahora), no estoy seguro de lo que está pasando. Mi sospecha sería algo así como que está introduciendo el captador, lo que da como resultado una copia de la variable, pero no estoy seguro. Hubiera esperado que eso no bloqueara. –

0

Para ampliar en Eric Petroelje's answer.

Si reescribimos el programa de la siguiente manera (el comportamiento es idéntico, pero evitar la función lambda hace que sea más fácil leer el desmontaje), podemos separarlo y ver lo que realmente significa "almacenar en caché el valor de un campo un registro"

class Foo 
{ 
    public bool Complete; // { get; set; } 
} 

class Program 
{ 
    static Foo foo = new Foo(); 

    static void ThreadProc() 
    { 
     bool toggle = false; 
     while (!foo.Complete) toggle = !toggle; 

     Console.WriteLine("Thread done"); 
    } 

    static void Main() 
    { 
     var t = new Thread(ThreadProc); 
     t.Start(); 
     Thread.Sleep(1000); 
     foo.Complete = true; 
     t.Join(); 
    } 
} 

obtenemos el siguiente comportamiento:

   Foo.Complete is a Field | Foo.Complete is a Property 
x86-RELEASE |  loops forever  |   completes 
x64-RELEASE |  completes   |   completes 

en x86 de liberación, el JIT CLR compila el tiempo (foo.Complete!) en este código:

Completo es un campo:

004f0153 a1f01f2f03  mov  eax,dword ptr ds:[032F1FF0h] # Put a pointer to the Foo object in EAX 
004f0158 0fb64004  movzx eax,byte ptr [eax+4] # Put the value pointed to by [EAX+4] into EAX (this basically puts the value of .Complete into EAX) 
004f015c 85c0   test eax,eax # Is EAX zero? (is .Complete false?) 
004f015e 7504   jne  004f0164 # If it is not, exit the loop 
# start of loop 
004f0160 85c0   test eax,eax # Is EAX zero? (is .Complete false?) 
004f0162 74fc   je  004f0160 # If it is, goto start of loop 

Las 2 últimas líneas son el problema. Si eax es cero, entonces se quedará allí en un ciclo infinito que dice "¿EAX es cero?", sin ningún código que cambie el valor de eax.

completa es una propiedad:

00220155 a1f01f3a03  mov  eax,dword ptr ds:[033A1FF0h] # Put a pointer to the Foo object in EAX 
0022015a 80780400  cmp  byte ptr [eax+4],0 # Compare the value at [EAX+4] with zero (is .Complete false?) 
0022015e 74f5   je  00220155 # If it is, goto 2 lines up 

En realidad, esto se parece más bonito de código. Mientras el JIT ha introducido el getter de propiedad (de lo contrario, vería algunas instrucciones call yendo a otras funciones) en un código simple para leer el campo Complete directamente, porque no está permitido almacenar en caché la variable, cuando genera el bucle, lee repetidamente la memoria una y otra vez, en lugar de sólo lectura inútilmente el registro

en x64 de liberación, el JIT CLR de 64 bits compila el tiempo (! foo.Complete) en el código

completa es un campo :

00140245 48b8d82f961200000000 mov rax,12962FD8h # put 12E12FD8h into RAX. 12E12FD8h is a pointer-to-a-pointer in some .NET static object table 
0014024f 488b00   mov  rax,qword ptr [rax] # Follow the above pointer; puts a pointer to the Foo object in RAX 
00140252 0fb64808  movzx ecx,byte ptr [rax+8] # Add 8 to the pointer to Foo object (it now points to the .Complete field) and put that value in ECX 
00140256 85c9   test ecx,ecx # Is ECX zero ? (is the .Complete field false?) 
00140258 751b   jne  00140275 # If nonzero/true, exit the loop 
0014025a 660f1f440000 nop  word ptr [rax+rax] # Do nothing! 
# start of loop 
00140260 48b8d82f961200000000 mov rax,12962FD8h # put 12E12FD8h into RAX. 12E12FD8h is a pointer-to-a-pointer in some .NET static object table 
0014026a 488b00   mov  rax,qword ptr [rax] # Follow the above pointer; puts a pointer to the Foo object in RAX 
0014026d 0fb64808  movzx ecx,byte ptr [rax+8] # Add 8 to the pointer to Foo object (it now points to the .Complete field) and put that value in ECX 
00140271 85c9   test ecx,ecx # Is ECX Zero ? (is the .Complete field true?) 
00140273 74eb   je  00140260 # If zero/false, go to start of loop 

Complete es una propiedad

00140250 48b8d82fe11200000000 mov rax,12E12FD8h # put 12E12FD8h into RAX. 12E12FD8h is a pointer-to-a-pointer in some .NET static object table 
0014025a 488b00   mov  rax,qword ptr [rax] # Follow the above pointer; puts a pointer to the Foo object in RAX 
0014025d 0fb64008  movzx eax,byte ptr [rax+8] # Add 8 to the pointer to Foo object (it now points to the .Complete field) and put that value in EAX 
00140261 85c0   test eax,eax # Is EAX 0 ? (is the .Complete field false?) 
00140263 74eb   je  00140250 # If zero/false, go to the start 

El JIT de 64 bits es hacer la misma cosa para ambas propiedades y campos, excepto cuando se trata de un campo que está "desenrollada" la primera iteración del bucle - esto básicamente pone un if(foo.Complete) { jump past the loop code } delante de él por alguna razón.

En ambos casos, es hacer una cosa similar a la JIT x 86 cuando se trata de una propiedad:
- Se inlines el método a una memoria de lectura directa - No almacenar en caché, y vuelve a leer el valor cada vez

No estoy seguro de si el CLR de 64 bits no puede almacenar el valor del campo en el registro como el de 32 bits, pero si lo es, no se molesta en hacerlo. Tal vez lo hará en el futuro?

En cualquier caso, esto ilustra cómo el comportamiento depende de la plataforma y está sujeto a cambios. Espero que ayude :-)

Cuestiones relacionadas