Ho bisogno di una barriera di memoria per un flag di notifica di modifica tra i thread?

Ho bisogno di un meccanismo di notifica delle modifiche molto veloce (nel senso “low cost for reader”, not “low latency”) tra thread per aggiornare una cache di lettura:

La situazione

Thread W (Writer) aggiorna una struttura dati ( S ) (nel mio caso un’impostazione in una mappa) solo una volta ogni tanto.

Thread R (Reader) mantiene una cache di S e lo legge molto frequentemente. Quando Thread W aggiorna S Thread R deve essere informato dell’aggiornamento in un tempo ragionevole (10-100 ms).

L’architettura è ARM, x86 e x86_64. Devo supportare C++03 con gcc 4.6 e versioni successive.

Codice

è qualcosa del genere:

 // variables shared between threads bool updateAvailable; SomeMutex dataMutex; std::string myData; // variables used only in Thread R std::string myDataCache; // Thread W SomeMutex.Lock(); myData = "newData"; updateAvailable = true; SomeMutex.Unlock(); // Thread R if(updateAvailable) { SomeMutex.Lock(); myDataCache = myData; updateAvailable = false; SomeMutex.Unlock(); } doSomethingWith(myDataCache); 

La mia domanda

Nella filettatura R non si verificano bloccaggi o barriere nel “percorso veloce” (nessun aggiornamento disponibile). È un errore? Quali sono le conseguenze di questo design?

Devo qualificare updateAvailable come volatile ?

R otterrà l’aggiornamento alla fine ?

La mia comprensione finora

È sicuro per quanto riguarda la coerenza dei dati?

Sembra un po ‘come “Double Checked Locking”. Secondo http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html è ansible utilizzare una barriera di memoria per risolverlo in C ++.

Tuttavia, la differenza principale qui è che la risorsa condivisa non viene mai toccata / letta nel percorso veloce Reader. Quando si aggiorna la cache, la coerenza è garantita dal mutex.

R otterrà l’aggiornamento?

Qui è dove diventa complicato. A quanto ho capito, la CPU che esegue Thread R potrebbe memorizzare cache updateAvailable indefinitamente, spostando in modo efficace il modo Read prima dell’effettiva istruzione if .

Quindi l’aggiornamento potrebbe richiedere fino al successivo svuotamento della cache, ad esempio quando è pianificato un altro thread o processo.

Devo qualificare updateAvailable come volatile ?

Poiché volatile non è correlato con il modello di threading in C ++, dovresti usare atomics per rendere il tuo programma rigorosamente standard-confirmant:

In C++11 o in un modo preferibile si usa atomic con memory_order_relaxed store / load:

 atomic updateAvailable; //Writer .... updateAvailable.store(true, std::memory_order_relaxed); //set (under mutex locked) // Reader if(updateAvailable.load(std::memory_order_relaxed)) // check { ... updateAvailable.store(false, std::memory_order_relaxed); // clear (under mutex locked) .... } 

gcc dal 4.7 supporta funzionalità simili con i suoi builtin atomici .

Per quanto riguarda gcc 4.6, sembra che non ci sia un modo rigorosamente confermante per eludere le recinzioni quando si accede updateAvailable variabile updateAvailable . In realtà, la recinzione di memoria è in genere molto più veloce di 10-100 ms di tempo. Quindi puoi usare i suoi builtin atomici :

 int updateAvailable = 0; //Writer ... __sync_fetch_and_or(&updateAvailable, 1); // set to non-zero .... //Reader if(__sync_fetch_and_and(&updateAvailable, 1)) // check, but never change { ... __sync_fetch_and_and(&updateAvailable, 0); // clear ... } 

È sicuro per quanto riguarda la coerenza dei dati?

Sì, è sicuro. La tua ragione è assolutamente corretta qui:

la risorsa condivisa non viene mai toccata / letta nel percorso veloce Reader.


Questo NON è un doppio controllo!

È esplicitamente indicato nella domanda stessa.

Nel caso in cui updateAvailable sia false, il thread Reader usa variabile myDataCache che è locale al thread (nessun altro thread lo usa). Con lo schema di blocco a doppio controllo, tutti i thread utilizzano direttamente l’object condiviso .

Perchè qui non esistono NECESSITÀ di barriere / barriere di memoria

L’unica variabile, accessibile contemporaneamente, è updateAvailable . myData variabile myData è accessibile con la protezione mutex, che fornisce tutte le myData necessarie. myDataCache è locale al thread Reader.

Quando il thread Reader vede la variabile updateAvailable come falsa , utilizza la variabile myDataCache , che viene modificata dal thread stesso . L’ordine del programma garantisce la corretta visibilità delle modifiche in quel caso.

Per quanto riguarda le garanzie di visibilità per gli updateAvailable variabili updateAvailable , lo standard C ++ 11 fornisce tali garanzie per le variabili atomiche anche senza recinti. 29,3 p13 dice:

Le implementazioni dovrebbero rendere i depositi atomici visibili ai carichi atomici entro un ragionevole lasso di tempo.

Jonathan Wakely ha confermato che questo paragrafo è applicato anche a memory_order_relaxed in chat .

Utilizzare gli std::atomic C ++ e rendere updateAvailable uno std::atomic . La ragione di ciò è che non è solo la CPU a vedere una vecchia versione della variabile, ma soprattutto il compilatore che non vede l’effetto collaterale di un altro thread e quindi non si preoccupa di recuperare la variabile in modo da non vedere mai il valore aggiornato nel thread. Inoltre, in questo modo ottieni una lettura atomica garantita, che non hai se leggi il valore.

Oltre a questo, potresti potenzialmente sbarazzarti del blocco, se per esempio il produttore produce sempre solo dati quando updateAvailable è falso, puoi sbarazzarti del mutex perché lo std::atomic<> impone l’ordinamento corretto delle letture e delle scritture . Se non è così, avrai ancora bisogno del lucchetto.

Devi usare una barriera di memoria qui. Senza il recinto, non vi è alcuna garanzia che gli aggiornamenti verranno mai visti sull’altro thread. In C ++ 03 si ha la possibilità di utilizzare il codice ASM specifico della piattaforma ( mfence su Intel, nessuna idea su ARM) o utilizzare le funzioni atomiche set / get fornite dal sistema operativo.