2012-05-01 8 views
6

Nel suo eccellente trattato sul threading in C#, Joseph Albahari ha proposto il seguente semplice programma per dimostrare perché abbiamo bisogno di usare qualche forma di scherma di memoria attorno ai dati letti e scritti da più filettature. Il programma non finisce mai, se si compila in modalità di rilascio e privo di eseguirlo senza debugger:variabile condivisa tra due thread si comporta in modo diverso dalla proprietà condivisa

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 
    } 

La mia domanda è, perché il seguente versione leggermente modificata del programma di cui sopra non è più bloccare a tempo indeterminato ??

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 che la segue ancora blocchi a tempo indeterminato:

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!!! 
    } 
} 

come fa il seguente:

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

Il titolo della tua domanda è più ampio di quello che deve essere per coprire il materiale in questione. Non tutto il codice è così semplice come questo. –

+0

Hai confrontato l'IL di entrambi i programmi? – Oded

+0

ho confrontato l'IL ma non ho visto nulla che potesse indurmi a una spiegazione – dmg

risposta

7

Nel primo esempio Complete è una variabile membro e potrebbe essere memorizzata nella cache nel registro per ogni thread. Dato che non stai usando il blocco, gli aggiornamenti a quella variabile potrebbero non essere scaricati nella memoria principale e l'altro thread vedrà un valore scaduto per quella variabile.

Nel secondo esempio, dove Complete è una proprietà, si sta effettivamente chiamando una funzione sull'oggetto Foo per restituire un valore. La mia ipotesi sarebbe che mentre le variabili semplici possono essere memorizzate nella cache nei registri, il compilatore potrebbe non ottimizzare sempre le proprietà reali in questo modo.

EDIT:

Per quanto riguarda l'ottimizzazione delle proprietà automatici - Non credo che ci sia qualcosa di garantito dalla specifica al riguardo. In sostanza, si sta facendo affidamento sul fatto che il compilatore/runtime sarà in grado di ottimizzare il getter/setter o meno.

Nel caso in cui si trovi sullo stesso oggetto, sembra che lo faccia. Nell'altro caso, sembra che non sia così. Ad ogni modo, non ci scommetterei. Il modo più semplice per risolvere ciò consiste nell'utilizzare una variabile membro semplice e contrassegnare come volotile per assicurarsi che sia sempre sincronizzato con la memoria principale.

+0

Come circa l'ultimo esempio che ho appena aggiunto? – dmg

+0

@dmg - modificato la mia risposta. Poiché le specifiche non forniscono alcuna garanzia in merito, si tratta di scommettere su come il compilatore possa o meno ottimizzare le proprietà automatiche. –

+0

sembra che sia quello che sta succedendo. se la proprietà Complete appartiene a questa classe, allora viene ottimizzata, ma se appartiene a una classe diversa, allora non lo è. – dmg

5

Questo perché nel primo frammento che hai fornito, hai fatto un'espressione lambda che si è chiuso sul valore booleano complete - così, quando il compilatore lo riscrive, acquisisce una copia del valore, non un riferimento. Allo stesso modo, nella seconda, sta acquisendo un riferimento anziché una copia, a causa della chiusura sull'oggetto Foo e quindi quando si modifica il valore sottostante, la modifica viene rilevata a causa del riferimento.

+0

Puoi spiegare come 'complete' viene catturato dal valore? Mi aspetto che venga catturato per riferimento, poiché questo è ciò che di solito accade in un'espressione lambda. –

+1

'bool' è un tipo di dati valore, quindi è impossibile acquisire per riferimento. – Tejs

+0

Ho appena aggiunto un altro snippet di codice. Il compilatore ottimizza il campo membro pubblico Completa allo stesso modo della variabile bool locale, ma non può eseguire la stessa ottimizzazione se il campo membro pubblico viene sostituito con una proprietà? – dmg

3

Le altre risposte spiegano cosa succede in termini tecnicamente corretti. Fammi vedere se posso spiegarlo in inglese.

Il primo esempio dice "Il ciclo fino a quando questa posizione variabile è vera". Il nuovo thread crea una copia di tale posizione variabile (perché è un tipo di valore) e procede al ciclo per sempre. Se la variabile fosse stata un tipo di riferimento, avrebbe creato una copia del riferimento, ma poiché il riferimento si è verificato per indicare la stessa posizione di memoria avrebbe funzionato.

Il secondo esempio dice "Loop fino a questo metodo (il getter) restituisce true." Il nuovo thread non può creare una copia di un metodo, quindi crea una copia del riferimento all'istanza della classe in questione e chiama ripetutamente il getter su quell'istanza finché non restituisce true (leggendo ripetutamente la stessa posizione variabile impostata per vero nel thread principale).

Il terzo esempio è uguale al primo. Il fatto che la variabile chiusa sia un membro di un'altra istanza di classe non è rilevante.

+0

Quindi immagino che nel quarto esempio il compilatore ottimizzi la chiamata alla proprietà get statica e la consideri come se fosse una copia di una variabile? – dmg

+0

Nel quarto esempio (mi dispiace, non l'avevo visto fino ad ora) non sono sicuro di cosa sta succedendo. Il mio sospetto sarebbe qualcosa di simile alla definizione del getter, risultante in una copia della variabile, ma non ne sono sicuro. Mi sarei aspettato di non bloccarlo. –

0

Per espandere su Eric Petroelje's answer.

Se riscriviamo il programma come segue (il comportamento è identico, ma evitando la funzione lambda rende più semplice la lettura del disassemblaggio), possiamo dissasigliarlo e vedere cosa significa in realtà "memorizzare il valore di un campo in 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(); 
    } 
} 

otteniamo il seguente comportamento:

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

in x86-release, il JIT CLR compila il tempo (foo.Complete!) in questo codice:

Completo è 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 

Le ultime 2 righe rappresentano il problema. Se eax è pari a zero, allora si limiterà a sedersi in un ciclo infinito che dice "è EAX zero?", senza che alcun codice modifichi mai il valore di eax!

Complete è una proprietà:

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 

Questo appare in realtà come il codice più bello. Mentre il JIT ha delineato il getter della proprietà (altrimenti vedresti alcune istruzioni call che si spostano su altre funzioni) in un semplice codice per leggere direttamente il campo Complete, perché non è consentito memorizzare nella cache la variabile, quando genera il ciclo, più volte si legge la memoria più e più volte, piuttosto che solo inutilmente la lettura del registro

in x64-release, il CLR JIT 64 bit compila il tempo (! foo.Complete) in questo codice

Complete è 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 è una proprietà

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 

Il JIT 64-bit sta facendo la stessa cosa per entrambe le proprietà ei campi, tranne quando si tratta di un campo che è "srotolato" la prima iterazione del ciclo - questo mette in pratica un if(foo.Complete) { jump past the loop code } di fronte ad essa per un po ' ragionare.

In entrambi i casi, si tratta di fare una cosa simile al JIT x86 quando si tratta di una proprietà:
- E 'inline il metodo a una memoria diretta lettura - E non memorizza nella cache, e rilegge il valore ogni volta

Non sono sicuro che il CLR a 64 bit non sia autorizzato a memorizzare nella cache il valore del campo nel registro come fa il 32 bit, ma se lo è, non si preoccupa di farlo. Forse lo farà in futuro?

In ogni caso, questo illustra come il comportamento dipende dalla piattaforma e soggetto a modifiche. Spero che aiuti :-)