Perché ho bisogno di una barriera di memoria?

C # 4 in a Nutshell (altamente consigliato btw) utilizza il seguente codice per dimostrare il concetto di MemoryBarrier (supponendo che A e B fossero eseguiti su thread diversi):

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); } } } 

menzionano che le Barriere 1 e 4 impediscono a questo esempio di scrivere 0 e le Barriere 2 e 3 forniscono una garanzia di freschezza : assicurano che se B corre dietro A, leggendo _complete si valuterà come vero .

Non lo capisco davvero. Penso di capire perché sono necessarie le Barriere 1 e 4: non vogliamo che la scrittura di _answer sia ottimizzata e posta dopo la scrittura su _complete (Barriera 1) e dobbiamo assicurarci che _answer non sia memorizzato nella cache (Barriera 4) . Penso anche di capire perché è necessario Barrier 3: se A ha funzionato fino a poco dopo aver scritto _complete = true , B dovrebbe comunque aggiornare _complete per leggere il valore corretto.

Non capisco però perché abbiamo bisogno di Barrier 2! Una parte di me dice che è forse perché Thread 2 (esecuzione B) è già in esecuzione fino (ma non incluso) se (_complete) e quindi è necessario assicurarsi che _complete sia aggiornato.

Tuttavia, non vedo come questo aiuti. Non è ancora ansible che _complete sia impostato su true in A, ma il metodo B vedrà una versione cache (falsa) di _complete ? Cioè, se il thread 2 ha eseguito il metodo B fino a dopo il primo MemoryBarrier e quindi il thread 1 ha eseguito il metodo A fino a _complete = true ma non oltre, e quindi il thread 1 è stato ripristinato e testato if (_complete) – che se non risulta in false ?

La barriera # 2 garantisce che la scrittura su _complete venga immediatamente commessa. Altrimenti potrebbe rimanere in uno stato in coda, il che significa che la lettura di _complete in B non vedrebbe la modifica causata da A anche se B effettivamente usato una lettura volatile.

Naturalmente, questo esempio non rende giustizia al problema perché A non fa altro dopo aver scritto su _complete che significa che la scrittura verrà comunicata immediatamente comunque poiché il thread termina precocemente.

La risposta alla tua domanda se il if potrebbe ancora valutare il false è sì esattamente per le ragioni che hai affermato. Ma, nota ciò che l’autore dice riguardo a questo punto.

Le barriere 1 e 4 impediscono a questo esempio di scrivere “0”. Le barriere 2 e 3 forniscono una garanzia di freschezza: assicurano che se B corri dopo A , leggendo _complete si valuti come true.

L’enfasi su “se B corre dietro A” è mia. Certamente potrebbe essere il caso che i due fili si intrecciano. Ma l’autore stava ignorando questo scenario presumibilmente per esprimere il suo punto su come Thread.MemoryBarrier più Thread.MemoryBarrier .

A proposito, ho avuto difficoltà a pensare ad un esempio sulla mia macchina in cui le barriere # 1 e # 2 avrebbero alterato il comportamento del programma. Questo perché il modello di memoria relativo alle scritture era forte nel mio ambiente. Forse, se avessi una macchina multiprocessore, usassi Mono, o avessi qualche altra configurazione diversa avrei potuto dimostrarlo. Certo, è stato facile dimostrare che rimuovere le barriere # 3 e # 4 ha avuto un impatto.

L’esempio non è chiaro per due motivi:

  1. È troppo semplice mostrare completamente cosa sta succedendo con le recinzioni.
  2. Albahari include i requisiti per architetture non-x86. Vedi MSDN : “MemoryBarrier è richiesto solo su sistemi multiprocessore con ordini di memoria deboli (ad esempio, un sistema che utilizza più processori Intel Itanium [che Microsoft non supporta più]).”.

Se consideri quanto segue, diventa più chiaro:

  1. Una barriera di memoria (qui le barriere complete – .Net non fornisce una mezza barriera) impedisce alle istruzioni di lettura / scrittura di saltare la recinzione (a causa di varie ottimizzazioni). Questo ci garantisce il codice dopo che il recinto verrà eseguito dopo il codice prima della recinzione.
  2. “Questa operazione di serializzazione garantisce che ogni istruzione di caricamento e di memorizzazione che precede nell’ordine del programma l’istruzione MFENCE è globalmente visibile prima che qualsiasi istruzione di caricamento o di memorizzazione che segue l’istruzione di MFENCE sia globalmente visibile.” Vedi qui
  3. Le CPU x86 hanno un modello di memoria forte e garantiscono che le scritture siano coerenti con tutti i thread / core (quindi le barriere # 2 e # 3 non sono necessarie su x86). Ma non ci è garantito che le letture e le scritture rimangano nella sequenza codificata, da qui la necessità delle barriere # 1 e # 4.
  4. Le barriere della memoria sono inefficienti e non devono essere utilizzate (vedere lo stesso articolo MSDN). Io personalmente uso Interlocked e volatile (assicurati di sapere come usarlo correttamente !!), che funzionano in modo efficiente e sono facili da capire.

Ps. Questo articolo spiega bene il funzionamento interno di x86.