Domanda

Ho una domanda circa il seguente codice di esempio (tratto da: http: // www.albahari.com/threading/part4.aspx#_NonBlockingSynch )

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

Questa è seguita con il seguente explantion:

  

"Le barriere 1 e 4 impediscono questo esempio di scrivere‘0’ Barriere 2 e 3 forniscono una garanzia di freschezza. si assicurano che se B rincorse A, la lettura _complete sarebbe valutata come vera."

ho capito come utilizzare le barriere di memoria influenzare il Reording istruzioni, ma che cosa è questo "freschezza gurarantee" che è menzionato?

In seguito in questo articolo, il seguente esempio viene anche utilizzato:

static void Main()
{
    bool complete = false; 
    var t = new Thread (() =>
    {
        bool toggle = false;
        while (!complete) 
        {
           toggle = !toggle;
           // adding a call to Thread.MemoryBarrier() here fixes the problem
        }

    });

    t.Start();
    Thread.Sleep (1000);
    complete = true;
    t.Join();  // Blocks indefinitely
}

Questo esempio è seguita con questa spiegazione:

  

"Questo programma non termina perché la variabile completo viene memorizzato nella cache in un registro CPU. Inserimento di una chiamata al Thread.MemoryBarrier all'interno del ciclo while (o bloccaggio attorno lettura completa) corregge l'errore."

Quindi, di nuovo ... quello che succede qui?

È stato utile?

Soluzione

Nel primo caso, Barriera 1 garantisce _answer è scritto PRIMA _complete. Indipendentemente da come il codice è scritto, o come il compilatore o CLR istruisce la CPU, bus di memoria lettura code / scrittura possono riordinare le richieste. La barriera in pratica dice "lavare la coda prima di continuare". Allo stesso modo, Barriera 4 assicura _answer viene letto dopo _complete. In caso contrario, CPU2 potrebbe riordinare le cose e vedere un vecchio _answer con un _complete "nuovo".

Barriere 2 e 3 sono, in un certo senso, inutile. Si noti che la spiegazione contiene la parola "dopo": vale a dire "... se B rincorse A, ...". Che cosa significa per B da eseguire dopo A? Se B e A sono sulla stessa CPU, poi certo, B può essere successiva. Ma in questo caso, stessa CPU significa che non problemi della barriera di memoria.

Quindi prendere in considerazione B e A in esecuzione su diverse CPU. Ora, molto simile relatività di Einstein, il concetto di confrontare i tempi in luoghi diversi / CPU non ha davvero senso. Un altro modo di pensare a questo proposito - si può scrivere codice che può dire se B rincorse A? Se è così, bene, probabilmente è stato utilizzato barriere di memoria per farlo. In caso contrario, non si può dire, e non ha senso chiedere. E 'anche simile al principio di Heisenburg -. Se si può osservare che, è stato modificato l'esperimento

Ma lasciando da parte la fisica, diciamo che si potrebbe aprire il cofano della macchina, e Vedere che la posizione in realtà ricordo di _complete era vero (perché A era corsa). Ora eseguire B. senza barriera 3, CPU2 potrebbe ancora non vedere _complete come vero. cioè non "fresco".

Ma probabilmente non è possibile aprire la macchina e guardare _complete. Né comunicare i risultati a B su CPU2. La vostra unica comunicazione è ciò che le CPU stessi stanno facendo. Quindi, se non in grado di determinare prima / dopo senza barriere, chiedendo "che cosa succede a B se viene eseguito dopo A, senza barriere" non ha senso .

A proposito, io non sono sicuro di quello che avete a disposizione in C #, ma ciò che è in genere fatto, e ciò che è realmente necessario per il campione di codice # 1 è un singolo ostacolo rilascio su scrittura, e un singolo ostacolo acquisire sul lettura :

void A()
{
   _answer = 123;
   WriteWithReleaseBarrier(_complete, true);  // "publish" values
}

void B()
{
   if (ReadWithAcquire(_complete))  // subscribe
   {  
      Console.WriteLine (_answer);
   }
}

La parola "subscribe" non è spesso usato per descrivere la situazione, ma "pubblicare" è. Vi suggerisco di leggere articoli di Herb Sutter sulla filettatura.

Questo mette le barriere in esattamente al posto giusto.

Per Esempio di codice # 2, questo non è davvero un problema di barriera di memoria, si tratta di un problema di ottimizzazione del compilatore - si sta mantenendo complete in un registro. Una barriera di memoria costringerebbe fuori, come sarebbe volatile, ma probabilmente così sarebbe chiamare una funzione esterna - se il compilatore non può dire se tale funzione modificata complete esterna o no, si ri-leggere dalla memoria. cioè forse passare l'indirizzo di complete a qualche funzione (definita da qualche parte dove il compilatore non può esaminare i dettagli):

while (!complete)
{
   some_external_function(&complete);
}

anche se la funzione non modifica complete, se il compilatore non è sicuro, avrà bisogno di ricaricare i suoi registri.

ossia la differenza tra il codice e il codice 1 2 è quel codice 1 ha solo problemi quando A e B sono in esecuzione sul thread separati. codice 2 potrebbe avere problemi anche su una singola macchina filettato.

In realtà, l'altra domanda sarebbe - il compilatore può rimuovere completamente il ciclo while? Se si pensa che complete è irraggiungibile da altro codice, perché no? vale a dire se ha deciso di spostare complete in un registro, potrebbe anche rimuovere completamente il circuito.

EDIT: Per rispondere il commento da OPC (la mia risposta è troppo grande per il blocco di commento):

Barriera 3 forze della CPU per irrigare qualsiasi lettura in attesa (e scrivere) richieste.

Quindi, immaginate se ci fosse qualche altra legge prima di leggere _complete:

void B {}
{
   int x = a * b + c * d; // read a,b,c,d
   Thread.MemoryBarrier();    // Barrier 3
   if (_complete)
   ...

Senza la barriera, la CPU potrebbe avere tutti questi 5 richieste di lettura'In attesa':

a,b,c,d,_complete

Senza la barriera, il processore potrebbe riordinare queste richieste per ottimizzare l'accesso di memoria (cioè se _complete e 'un' erano sulla stessa linea di cache o qualcosa del genere).

Con la barriera, la CPU ottiene a, b, c, d ritorno dalla memoria prima _complete è anche mettere in una richiesta. GARANTIRE 'B' (per esempio) viene letta PRIMA _complete -. Ovvero nessun riordino

La domanda è - che differenza fa

?

Se a, b, c, d sono indipendenti dal _complete, quindi non importa. Tutta la barriera non è le cose rallentano. Quindi sì, _complete si legge dopo . Quindi i dati sono più fresco . Mettere un sonno (100) o qualche occupato-attendere-loop in là prima che la lettura renderebbe 'fresco', come pure! : -)

Quindi il punto è - tenerlo relativo. Ha bisogno dei dati da leggere / scrivere prima / dopo rispetto ad alcuni altri dati o no? Questa è la domanda.

E per non mettere giù l'autore di questo articolo - lo fa menzione "se B corse dietro A ...". E 'solo che non è esattamente chiaro se egli immagina che B dopo A è fondamentale per il codice, osservabile da in codice, o semplicemente irrilevante.

Altri suggerimenti

Codice di esempio # 1:

Ogni core contiene una cache con una copia di una porzione di memoria. Si può prendere un po 'di tempo per la cache da aggiornare. Le barriere di memoria garantiscono che le cache sono sincronizzati con la memoria principale. Ad esempio, se non si dispone di barriere 2 e 3 qui, prendere in considerazione questa situazione:

processore 1 piste A (). Si scrive il nuovo valore di _complete alla sua cache (ma non necessariamente alla memoria principale ancora).

processore 2 piste B (). E 'legge il valore di _complete. Se questo valore è stato in precedenza nella sua cache, potrebbe non essere fresco (vale a dire, non è sincronizzato con la memoria principale), quindi non sarebbe ottenere il valore aggiornato.

Codice di esempio # 2:

In genere, le variabili sono memorizzate nella memoria. Tuttavia, si supponga che un valore viene letto più volte in una singola funzione: Come ottimizzazione, il compilatore può decidere di leggerlo in un registro della CPU una volta, e quindi accedere alla ogni volta che è necessario registrarsi. Questo è molto più veloce, ma impedisce la funzione di rilevare modifiche alla variabile da un altro thread.

La barriera memoria qui costringe la funzione di rileggere il valore variabile dalla memoria.

Calling Thread.MemoryBarrier () aggiorna immediatamente le cache di registro con i valori attuali delle variabili.

Nel primo esempio, la "freschezza" per _complete è fornito chiamando il metodo giusto dopo aver impostato e giusto prima di utilizzarlo. Nel secondo esempio, il valore iniziale per la false complete variabile verrà memorizzato nella cache in proprio spazio del thread e deve essere risincronizzare per vedere immediatamente il valore effettivo "fuori" dal filo conduttore "interno".

La garanzia "freschezza" significa semplicemente che le barriere 2 e 3 forza i valori di _complete siano visibili appena possibile anziché ogni volta che capita di essere scritti nella memoria.

In realtà inutili dal punto di vista consistenza, poiché gli ostacoli 1 e 4 assicurano che answer verrà letto dopo aver letto complete.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top