2009-10-17 12 views
18

Sto leggendo il post di Joe Duffy su Volatile reads and writes, and timeliness, e sto cercando di capire qualcosa circa l'ultimo esempio di codice nel post:Interlocked.CompareExchange utilizza una barriera di memoria?

while (Interlocked.CompareExchange(ref m_state, 1, 0) != 0) ; 
m_state = 0; 
while (Interlocked.CompareExchange(ref m_state, 1, 0) != 0) ; 
m_state = 0; 
… 

Quando viene eseguita la seconda operazione cmpxchg vuol usare una barriera di memoria per garantire che il valore di m_state sia effettivamente l'ultimo valore scritto su di esso? O userà solo qualche valore che è già memorizzato nella cache del processore? (supponendo che m_state non sia dichiarato come volatile).
Se ho capito bene, se CMPXCHG non usa una barriera di memoria, allora l'intera procedura di acquisizione del lucchetto non sarà corretta poiché è altamente probabile che il thread che è stato il primo ad acquisire il lucchetto, sarà quello che acquisire tutte le seguenti serrature. Ho capito bene o sto perdendo qualcosa qui?

Modifica: La domanda principale è in realtà se la chiamata a CompareExchange causerà una barriera di memoria prima di tentare di leggere il valore di m_state. Quindi se l'assegnazione di 0 sarà visibile a tutti i thread quando provano a chiamare di nuovo CompareExchange.

risposta

22

Qualsiasi istruzione x86 che ha il prefisso ha la barriera di memoria piena . Come mostrato la risposta di Abel, Interlocked * APIs e CompareExchanges utilizzano il blocco - istruzioni prefissate come lock cmpxchg. Quindi, implica la barriera della memoria.

Sì, Interlocked.CompareExchange utilizza una barriera di memoria.

Perché? Perché i processori x86 l'hanno fatto. Da Intel Volume 3A: System Programming Guide Part 1, Sezione 7.1.2.2:

Per i processori della famiglia P6, operazioni bloccate serializzare tutte le operazioni di carico e memorizzare eccezionali (cioè, attendere per loro di completare). Questa regola vale anche per i processori Pentium 4 e Intel Xeon, con un'eccezione. Le operazioni di caricamento che fanno riferimento a tipi di memoria debolmente ordinati (come il tipo di memoria WC) potrebbero non essere serializzate.

volatile non ha nulla a che fare con questa discussione. Si tratta di operazioni atomiche; per supportare le operazioni atomiche nella CPU, x86 garantisce tutti i carichi e i depositi precedenti da completare.

+0

Vale la pena menzionare che fornisce FULL FENCE e non mezzo recinto. –

10

ref non rispetta le solite volatile regole, soprattutto in cose come:

volatile bool myField; 
... 
RunMethod(ref myField); 
... 
void RunMethod(ref bool isDone) { 
    while(!isDone) {} // silly example 
} 

Qui, RunMethod non è garantita da individuare cambiamenti esterni al isDone anche se il campo sottostante (myField) è volatile; RunMethod non lo sa, quindi non ha il codice giusto.

Tuttavia! Questo dovrebbe essere un non-problema:

  • se si utilizza Interlocked, quindi utilizzare Interlocked per tutto accesso al campo
  • se si utilizza lock, quindi utilizzare lock per tutto accesso al campo

Seguire queste regole e dovrebbe funzionare OK.


Per la modifica; Sì, il comportamento è una parte critica di Interlocked. Ad essere onesti, non so come sia implementato (barriera di memoria, ecc. - si noti che sono metodi "InternalCall", quindi non posso controllare ;-p) - ma sì: gli aggiornamenti da un thread saranno immediatamente visibili a tutti gli altri finchè usano i metodi Interlocked (da qui il mio punto sopra).

+0

Non sto chiedendo informazioni sui volatili, ma solo se è necessario un Interlocked.Exchange quando si rilascia il blocco (o, Thread.VolatileWrite sarà più appropriato). e l'unico problema che potrebbe sorgere da questo codice è l'abitudine di "ingiustizia" (come menziona Joe all'inizio di questo post) –

+0

@Marc: la fonte dei metodi di InternalCall può essere visualizzata (per la maggior parte) attraverso il Fonte condivisa CLI SSCLI, aka Rotor. Interlocked.CompareExchange è spiegato in questa interessante lettura: http://www.moserware.com/2008/09/how-do-locks-lock.html – Abel

2

Le funzioni interbloccate sono garantite per arrestare il bus e la cpu mentre risolve gli operandi. La conseguenza immediata è che nessun interruttore di thread, sulla tua cpu o su un altro, interromperà la funzione interbloccata nel mezzo della sua esecuzione.

Poiché si passa un riferimento alla funzione C#, il codice dell'assembler sottostante funzionerà con l'indirizzo del numero intero effettivo, quindi l'accesso variabile non verrà ottimizzato. Funzionerà esattamente come previsto.

Edit: Ecco un link che spiega il comportamento delle istruzioni asm meglio: http://faydoc.tripod.com/cpu/cmpxchg.htm
Come si può vedere, il bus è in stallo forzando un ciclo di scrittura, in modo da tutti gli altri "fili" (leggi: altri core CPU) quello proverebbe a usare il bus nello stesso momento sarebbe messo in una coda di attesa.

+0

In realtà, il contrario (parzialmente) è vero. Interlocked esegue un'operazione atomica e utilizza l'istruzione di assemblaggio 'cmpxchg'. Non richiede di mettere gli altri thread in uno stato di attesa, quindi è molto performante. Vedere la sezione "Inside InternalCall" in questa pagina: http://www.moserware.com/2008/09/how-do-locks-lock.html – Abel

2

MSDN dice circa le funzioni API Win32: "maggior parte delle funzioni intrecciate forniscono le barriere di memoria completo su tutte le piattaforme Windows"

(le eccezioni sono funzioni asservito esplicite semantica Acquisisci/Stampa)

Da ciò vorrei concludere che l'Interlocked del C# runtime offre le stesse garanzie, poiché sono documentati con un comportamento identico (e risolvono le affermazioni intrinseche della CPU sulle piattaforme che conosco). Sfortunatamente, con la tendenza di MSDN a pubblicare campioni invece di documentazione, non è esplicitato esplicitamente.

6

Sembra esserci un confronto con le funzioni API Win32 con lo stesso nome, ma questo thread riguarda esclusivamente la classe C# Interlocked. Dalla sua stessa descrizione, è garantito che le sue operazioni siano atomiche. Non sono sicuro di come ciò si traduca in "barriere della memoria completa" come menzionato in altre risposte qui, ma giudicate voi stessi.

Su sistemi monoprocessore, niente di speciale accade, c'è solo una singola istruzione:

FASTCALL_FUNC CompareExchangeUP,12 
     _ASSERT_ALIGNED_4_X86 ecx 
     mov  eax, [esp+4] ; Comparand 
     cmpxchg [ecx], edx 
     retn 4    ; result in EAX 
FASTCALL_ENDFUNC CompareExchangeUP 

Ma su sistemi multiprocessore, un blocco hardware è usato per prevenire altri core per accedere ai dati, allo stesso tempo:

FASTCALL_FUNC CompareExchangeMP,12 
     _ASSERT_ALIGNED_4_X86 ecx 
     mov  eax, [esp+4] ; Comparand 
    lock cmpxchg [ecx], edx 
     retn 4    ; result in EAX 
FASTCALL_ENDFUNC CompareExchangeMP 

Una lettura interessante con qua e là alcune conclusioni errate, ma tutto sommato eccellente sull'argomento è questo blog post on CompareExchange.

0

Secondo ECMA-335 (sezione I.12.6.5):

5. Operazioni atomiche esplicite. La libreria di classi fornisce una varietà di operazioni atomiche nella classe System.Threading.Interlocked . Queste operazioni (ad esempio Incremento, Decrement, Exchange e CompareExchange) eseguono operazioni implicite di acquisizione/rilascio .

Quindi, queste operazioni seguono il principio di minor stupore.

Problemi correlati