Quando usare volatile con multi threading?


131

Se ci sono due thread che accedono a una variabile globale, molti tutorial dicono che rendono la variabile volatile per evitare che il compilatore memorizzi nella cache la variabile in un registro e che quindi non venga aggiornato correttamente. Tuttavia, due thread che accedono a una variabile condivisa sono qualcosa che richiede protezione tramite un mutex, non è vero? Ma in quel caso, tra il blocco del thread e il rilascio del mutex, il codice si trova in una sezione critica in cui solo un thread può accedere alla variabile, nel qual caso la variabile non deve essere volatile?

Quindi quindi qual è l'uso / lo scopo di volatile in un programma multi-thread?


3
In alcuni casi, non si desidera / non è necessaria la protezione da parte del mutex.
Stefan Mai,

4
A volte va bene avere una condizione di gara, a volte no. Come stai usando questa variabile?
David Heffernan,

3
@ David: un esempio di quando "va bene" fare una gara, per favore?
John Dibling,

6
@John Ecco qui. Immagina di avere un thread di lavoro che sta elaborando una serie di attività. Il thread di lavoro incrementa un contatore ogni volta che termina un'attività. Il thread principale legge periodicamente questo contatore e aggiorna l'utente con notizie sullo stato di avanzamento. Finché il contatore è correttamente allineato per evitare strappi, non è necessario sincronizzare l'accesso. Sebbene ci sia una razza, è benigna.
David Heffernan,

5
@John L'hardware su cui viene eseguito questo codice garantisce che le variabili allineate non possano soffrire di strappi. Se il lavoratore aggiorna da n a n + 1 mentre legge il lettore, al lettore non importa se ottiene n o n + 1. Non verranno prese decisioni importanti poiché viene utilizzato solo per la comunicazione dei progressi.
David Heffernan,

Risposte:


168

Risposta breve e rapida : volatileè (quasi) inutile per la programmazione di applicazioni multithread indipendente dalla piattaforma. Non fornisce alcuna sincronizzazione, non crea recinti di memoria, né garantisce l'ordine di esecuzione delle operazioni. Non rende le operazioni atomiche. Non rende il tuo codice magicamente sicuro. volatilepotrebbe essere la struttura più incompresa in tutto il C ++. Vedi questo , questo e questo per maggiori informazioni suvolatile

D'altra parte, volatileha qualche utilità che potrebbe non essere così ovvio. Può essere usato allo stesso modo in cui si userebbe constper aiutare il compilatore a mostrarti dove potresti commettere un errore nell'accedere ad alcune risorse condivise in modo non protetto. Questo uso è discusso da Alexandrescu in questo articolo . Tuttavia, questo sta fondamentalmente usando il sistema di tipo C ++ in un modo che viene spesso visto come un accorgimento e può evocare un comportamento indefinito.

volatileè stato specificamente progettato per essere utilizzato quando si interfaccia con hardware mappato in memoria, gestori di segnali e istruzioni di codice macchina setjmp. Ciò è volatiledirettamente applicabile alla programmazione a livello di sistema piuttosto che alla normale programmazione a livello di applicazioni.

Lo standard C ++ del 2003 non afferma che si volatileapplica alcun tipo di semantica Acquire o Release sulle variabili. In effetti, lo standard è completamente silenzioso su tutte le questioni relative al multithreading. Tuttavia, piattaforme specifiche applicano la semantica Acquire and Release sulle volatilevariabili.

[Aggiornamento per C ++ 11]

Il C ++ 11 standard ora fa acknowledge multithreading direttamente nel modello di memoria e la lanuage, e fornisce funzionalità di libreria per affrontare in modo indipendente dalla piattaforma. Tuttavia, la semantica di volatilenon è ancora cambiata. volatilenon è ancora un meccanismo di sincronizzazione. Bjarne Stroustrup dice altrettanto in TCPPPL4E:

Non utilizzare volatilese non nel codice di basso livello che si occupa direttamente dell'hardware.

Non dare per scontato volatileun significato speciale nel modello di memoria. Non è così. Non è - come in alcune lingue successive - un meccanismo di sincronizzazione. Per ottenere la sincronizzazione, utilizzare atomic, a mutexo a condition_variable.

[/ Fine aggiornamento]

Quanto sopra si applica al linguaggio C ++ stesso, come definito dalla norma del 2003 (e ora dalla norma del 2011). Alcune piattaforme specifiche tuttavia aggiungono funzionalità aggiuntive o restrizioni a ciò che volatilefa. Ad esempio, in MSVC 2010 (almeno) la semantica Acquire and Release si applica a determinate operazioni sulle volatilevariabili. Da MSDN :

Durante l'ottimizzazione, il compilatore deve mantenere l'ordinamento tra riferimenti ad oggetti volatili e riferimenti ad altri oggetti globali. In particolare,

Una scrittura su un oggetto volatile (scrittura volatile) ha la semantica Release; un riferimento a un oggetto globale o statico che si verifica prima di una scrittura su un oggetto volatile nella sequenza di istruzioni si verificherà prima di quella scrittura volatile nel binario compilato.

Una lettura di un oggetto volatile (lettura volatile) ha Acquisire semantica; un riferimento a un oggetto globale o statico che si verifica dopo una lettura della memoria volatile nella sequenza delle istruzioni si verificherà dopo quella lettura volatile nel file binario compilato.

Tuttavia, si potrebbe prendere atto del fatto che se si segue il link qui sopra, v'è un certo dibattito nei commenti sul fatto o meno la semantica acquisiscono / rilascio in realtà si applicano in questo caso.


19
Una parte di me vuole sottovalutare questo a causa del tono condiscendente della risposta e del primo commento. "volatile è inutile" è simile a "l'allocazione manuale della memoria è inutile". Se riesci a scrivere un programma multithreading senza di volatileesso è perché sei rimasto sulle spalle delle persone che erano solite volatileimplementare le librerie di thread.
Ben Jackson,

20
@Ben solo perché qualcosa sfida le tue convinzioni non lo rende condiscendente
David Heffernan,

39
@Ben: no, leggi cosa favolatile effettivamente in C ++. Quello che @ John ha detto è corretto , fine della storia. Non ha nulla a che fare con il codice dell'applicazione contro il codice della libreria o "ordinari" contro "programmatori onniscienti simili a dio". è inutile e inutile per la sincronizzazione tra thread. Le librerie di threading non possono essere implementate in termini di ; deve comunque fare affidamento su dettagli specifici della piattaforma e quando si fa affidamento su di essi non è più necessario . volatilevolatilevolatile
jalf

6
@jalf: "volatile non è necessario e inutile per la sincronizzazione tra thread" (che è quello che hai detto) non è la stessa cosa di "volatile è inutile per la programmazione multithread" (che è ciò che John ha detto nella risposta). Hai ragione al 100%, ma non sono d'accordo con John (parzialmente) - volatile può ancora essere usato per la programmazione multithread (per un set molto limitato di attività)

4
@GMan: tutto ciò che è utile è utile solo in un determinato insieme di requisiti o condizioni. Volatile è utile per la programmazione multithread in una serie rigorosa di condizioni (e in alcuni casi, potrebbe persino essere migliore (per una definizione di migliore) rispetto alle alternative). Dici "ignorando questo e ..." ma il caso in cui volatile è utile per il multithreading non ignora nulla. Hai inventato qualcosa che non ho mai rivendicato. Sì, l'utilità di volatile è limitata, ma esiste - ma possiamo tutti concordare sul fatto che NON è utile per la sincronizzazione.

31

(Nota del redattore: in C ++ 11 volatilenon è lo strumento giusto per questo lavoro e ha ancora UB data-race. Utilizzare std::atomic<bool>con std::memory_order_relaxedcarichi / negozi per farlo senza UB. Sulle implementazioni reali si compilerà nello stesso modo di volatile. Ho aggiunto una risposta con maggiori dettagli e anche affrontare le idee sbagliate nei commenti secondo cui la memoria ordinata in modo debole potrebbe essere un problema per questo caso d'uso: tutte le CPU del mondo reale hanno una memoria condivisa coerente, quindi volatilefunzioneranno per questo su implementazioni C ++ reali. non farlo.

Qualche discussione nei commenti sembra parlare di altri casi d'uso in cui avresti bisogno di qualcosa di più forte dell'atomica rilassata. Questa risposta sottolinea già che volatilenon ti dà alcun ordine.)


Volatile è occasionalmente utile per il seguente motivo: questo codice:

/* global */ bool flag = false;

while (!flag) {}

è ottimizzato da gcc per:

if (!flag) { while (true) {} }

Il che è ovviamente errato se la bandiera è scritta dall'altro thread. Si noti che senza questa ottimizzazione il meccanismo di sincronizzazione probabilmente funziona (a seconda dell'altro codice potrebbero essere necessarie alcune barriere di memoria) - non è necessario uno scenario mutex in 1 produttore - 1 consumatore.

Altrimenti la parola chiave volatile è troppo strana per essere utilizzabile - non fornisce alcuna garanzia per l'ordinamento della memoria rispetto agli accessi volatili e non volatili e non fornisce alcuna operazione atomica - vale a dire non si ottiene aiuto dal compilatore con la parola chiave volatile tranne la cache del registro disabilitata .


4
Se ricordo, C ++ 0x atomic, è pensato per fare correttamente ciò che molte persone credono (erroneamente) di volatile.
David Heffernan,

14
volatilenon impedisce il riordino degli accessi alla memoria. volatilegli accessi non verranno riordinati l'uno rispetto all'altro, ma non offrono alcuna garanzia sul riordino rispetto ai non volatileoggetti, e quindi sono sostanzialmente inutili anche come bandiere.
jalf

14
@Ben: penso che tu l'abbia capovolto. La folla "volatile è inutile" si basa sul semplice fatto che la volatile non protegge dal riordino , il che significa che è assolutamente inutile per la sincronizzazione. Altri approcci potrebbero essere ugualmente inutili (come dici tu, l'ottimizzazione del codice del link-time potrebbe consentire al compilatore di sbirciare nel codice che pensavi che il compilatore avrebbe trattato come una scatola nera), ma ciò non risolve le carenze di volatile.
jalf

15
@jalf: vedi l'articolo di Arch Robinson (linkato altrove in questa pagina), decimo commento (di "Spud"). Fondamentalmente, il riordino non cambia la logica del codice. Il codice pubblicato utilizza il flag per annullare un'attività (anziché per segnalare che l'attività è stata eseguita), quindi non importa se l'attività viene annullata prima o dopo il codice (ad es .: while (work_left) { do_piece_of_work(); if (cancel) break;}se l'annullamento viene riordinato all'interno del ciclo, la logica è ancora valida, avevo un pezzo di codice che funzionava in modo simile: se il thread principale vuole terminare, imposta il flag per altri thread, ma non ...

15
... importa se gli altri thread eseguono alcune iterazioni extra dei loro cicli di lavoro prima che terminino, purché ciò avvenga ragionevolmente subito dopo l'impostazione della bandiera. Naturalmente, questo è l'UNICO uso che mi viene in mente e la sua nicchia piuttosto (e potrebbe non funzionare su piattaforme in cui la scrittura su una variabile volatile non rende visibile la modifica ad altri thread, anche se almeno su x86 e x86-64 questo lavori). Certamente non consiglierei a nessuno di farlo davvero senza una buona ragione, sto solo dicendo che un'istruzione generale come "volatile non è MAI utile nel codice multithread" non è corretta al 100%.

16

In C ++ 11, normalmente non utilizzare mai volatileper il threading, solo per MMIO

Ma TL: DR, "funziona" in qualche modo come su un atomico con mo_relaxedhardware con cache coerenti (cioè tutto); è sufficiente impedire ai compilatori di mantenere i registri nei vari registri. atomicnon servono barriere di memoria per creare atomicità o visibilità inter-thread, solo per far sì che il thread corrente attenda prima / dopo un'operazione per creare un ordine tra gli accessi di questo thread a diverse variabili. mo_relaxednon ha mai bisogno di barriere, basta caricare, archiviare o RMW.

Per gli atomici roll-your-own con volatile(e inline-asm per barriere) nei brutti vecchi tempi prima di C ++ 11 std::atomic, volatileera l'unico modo buono per far funzionare alcune cose . Ma dipendeva da molte ipotesi su come funzionavano le implementazioni e non era mai garantito da alcuno standard.

Ad esempio, il kernel Linux utilizza ancora la propria atomica a rotazione manuale con volatile, ma supporta solo alcune implementazioni C specifiche (GNU C, clang e forse ICC). In parte è a causa delle estensioni GNU C e della sintassi asm inline e della semantica, ma anche perché dipende da alcuni presupposti su come funzionano i compilatori.

È quasi sempre la scelta sbagliata per i nuovi progetti; puoi usare std::atomic(with std::memory_order_relaxed) per ottenere un compilatore per emettere lo stesso codice macchina efficiente che potresti avere volatile. std::atomiccon mo_relaxedobsoletes volatileper scopi di threading. (tranne forse per aggirare i bug di ottimizzazione persi con atomic<double>alcuni compilatori .)

L'implementazione interna di std::atomiccompilatori tradizionali (come gcc e clang) non si limita a utilizzare volatileinternamente; i compilatori espongono direttamente le funzioni integrate di caricamento atomico, archivio e RMW. (es. built -in GNU C__atomic che operano su oggetti "semplici").


Volatile è utilizzabile in pratica (ma non farlo)

Detto questo, volatileè utilizzabile in pratica per cose come una exit_nowbandiera su tutte le (?) Implementazioni C ++ esistenti su CPU reali, a causa del modo in cui funzionano le CPU (cache coerenti) e condivide ipotesi su come volatiledovrebbe funzionare. Ma non molto altro, e non è raccomandato. Lo scopo di questa risposta è spiegare come funzionano effettivamente le CPU e le implementazioni C ++ esistenti. Se non ti interessa, tutto quello che devi sapere è che std::atomiccon i viola mo_relax volatileper il threading.

(Lo standard ISO C ++ è piuttosto vago su di esso, solo dicendo che gli volatileaccessi dovrebbero essere valutati rigorosamente secondo le regole della macchina astratta C ++, non ottimizzati lontano. Dato che le implementazioni reali usano lo spazio di indirizzi di memoria della macchina per modellare lo spazio di indirizzi C ++, questo significa che le volatileletture e le assegnazioni devono essere compilate per caricare / memorizzare le istruzioni per accedere alla rappresentazione dell'oggetto in memoria.)


Come sottolinea un'altra risposta, un exit_nowflag è un semplice caso di comunicazione tra thread che non necessita di sincronizzazione : non sta pubblicando che i contenuti dell'array sono pronti o qualcosa del genere. Solo un negozio notato prontamente da un carico non ottimizzato in un altro thread.

    // global
    bool exit_now = false;

    // in one thread
    while (!exit_now) { do_stuff; }

    // in another thread, or signal handler in this thread
    exit_now = true;

Senza volatile o atomico, la regola as-if e l'assunzione di nessun UB data-race consente a un compilatore di ottimizzarlo in asm che controlla il flag solo una volta , prima di entrare (o meno) in un loop infinito. Questo è esattamente ciò che accade nella vita reale per i veri compilatori. (E di solito ottimizza molto do_stuffperché il ciclo non esce mai, quindi qualsiasi codice successivo che potrebbe aver usato il risultato non è raggiungibile se entriamo nel ciclo).

 // Optimizing compilers transform the loop into asm like this
    if (!exit_now) {        // check once before entering loop
        while(1) do_stuff;  // infinite loop
    }

Il programma di multithreading bloccato in modalità ottimizzata ma funziona normalmente in -O0 è un esempio (con descrizione dell'output asm di GCC) di come esattamente ciò accada con GCC su x86-64. Anche la programmazione MCU - l'ottimizzazione O ++ C ++ si interrompe mentre si esegue il loop su electronics.SE mostra un altro esempio.

Normalmente desideriamo ottimizzazioni aggressive che CSE e paranco caricano dai loop, anche per le variabili globali.

Prima di C ++ 11, volatile bool exit_nowc'era un modo per farlo funzionare come previsto (su normali implementazioni C ++). Ma in C ++ 11, UB data-race si applica ancora, volatilequindi in realtà non è garantito dallo standard ISO per funzionare ovunque, anche assumendo cache coerenti HW.

Si noti che per i tipi più ampi, volatilenon garantisce la mancanza di lacerazione. Ho ignorato questa distinzione qui boolperché è un problema sulle normali implementazioni. Ma questo è anche parte del motivo per cui volatileè ancora soggetto a UB data-race anziché essere equivalente a atomico rilassato.

Si noti che "come previsto" non significa che il thread in exit_nowattesa attende che l'altro thread esca effettivamente. O anche che attende che il exit_now=truenegozio volatile sia persino visibile a livello globale prima di continuare con le operazioni successive in questo thread. ( atomic<bool>con il valore predefinito mo_seq_cstlo farebbe attendere prima di caricare seq_cst in un secondo momento. In molti ISA otterresti una barriera completa dopo il negozio).

C ++ 11 fornisce un modo non UB che compila lo stesso

Un flag "continua a correre" o "esci adesso" dovrebbe essere usato std::atomic<bool> flagconmo_relaxed

utilizzando

  • flag.store(true, std::memory_order_relaxed)
  • while( !flag.load(std::memory_order_relaxed) ) { ... }

ti darà esattamente la stessa domanda (senza costose istruzioni sulla barriera) da cui potresti ottenere volatile flag.

Oltre a non strappare, atomicti dà anche la possibilità di archiviare in un thread e caricarlo in un altro senza UB, quindi il compilatore non può sollevare il carico da un loop. (L'assunzione di nessun UB di data-race è ciò che consente le ottimizzazioni aggressive che desideriamo per gli oggetti non volatili non atomici.) Questa caratteristica atomic<T>è praticamente la stessa di quella volatiledei carichi puri e dei negozi puri.

atomic<T>anche fare +=e così via in operazioni atomiche RMW (significativamente più costose di un carico atomico in un temporaneo, operare, quindi un negozio atomico separato. Se non si desidera un RMW atomico, scrivere il codice con un temporaneo locale).

Con l' seq_cstordinamento predefinito che otterresti while(!flag), aggiunge anche garanzie d'ordine. accessi non atomici e ad altri accessi atomici.

(In teoria, lo standard ISO C ++ non esclude l'ottimizzazione in fase di compilazione della Atomics. Ma in pratica compilatori non lo fanno perché non c'è modo di controllo quando non sarebbe ok. Ci sono alcuni casi in cui addirittura volatile atomic<T>non potrebbe avere abbastanza controllo sull'ottimizzazione dell'atomica se i compilatori hanno ottimizzato, quindi per ora i compilatori no. Vedi Perché i compilatori non uniscono std ridondanti :: scritture atomiche? Nota che wg21 / p0062 consiglia di non utilizzare volatile atomicnel codice corrente per evitare l'ottimizzazione di Atomics.)


volatile funziona davvero per questo su CPU reali (ma ancora non lo usa)

anche con modelli di memoria debolmente ordinati (non x86) . Ma in realtà non usarlo, l'uso atomic<T>con mo_relaxedinvece !! Il punto di questa sezione è quello di affrontare le idee sbagliate su come funzionano le CPU reali, non per giustificare volatile. Se stai scrivendo codice senza blocco, probabilmente ti preoccupi delle prestazioni. Comprendere le cache e i costi della comunicazione tra thread è generalmente importante per una buona prestazione.

Le CPU reali hanno cache / memoria condivisa coerenti: dopo che un archivio da un core diventa visibile a livello globale, nessun altro core può caricare un valore non aggiornato. (Vedi anche Myths Programmersatomic<T> Crede nella cache della CPU che parla di volatili Java, equivalente a C ++ con ordine di memoria seq_cst.)

Quando dico load , intendo un'istruzione asm che accede alla memoria. Questo è ciò che volatilegarantisce un accesso, e non è la stessa cosa della conversione da lvalue a rvalue di una variabile C ++ non atomica / non volatile. (es. local_tmp = flago while(!flag)).

L'unica cosa che devi sconfiggere sono le ottimizzazioni in fase di compilazione che non si ricaricano affatto dopo il primo controllo. Qualsiasi carico + controllo su ogni iterazione è sufficiente, senza alcun ordine. Senza sincronizzazione tra questo thread e il thread principale, non è significativo parlare di quando si è verificato esattamente l'archivio o dell'ordinamento del carico wrt. altre operazioni nel loop. Solo quando è visibile a questo thread è ciò che conta. Quando vedi il flag exit_now impostato, esci. La latenza inter-core su un tipico Xeon x86 può essere qualcosa come 40 ns tra nuclei fisici separati .


In teoria: thread C ++ su hardware senza cache coerenti

Non vedo in alcun modo che ciò potrebbe essere remotamente efficiente, con solo ISO C ++ puro senza richiedere al programmatore di eseguire flush espliciti nel codice sorgente.

In teoria potresti avere un'implementazione C ++ su una macchina che non era così, richiedendo flush espliciti generati dal compilatore per rendere le cose visibili ad altri thread su altri core . (O per le letture di non usare una copia forse stantia). Lo standard C ++ non lo rende impossibile, ma il modello di memoria di C ++ è progettato per essere efficiente su macchine a memoria condivisa coerenti. Ad esempio lo standard C ++ parla anche di "coerenza lettura-lettura", "coerenza scrittura-lettura", ecc. Una nota nello standard indica anche la connessione all'hardware:

http://eel.is/c++draft/intro.races#19

[Nota: i quattro requisiti di coerenza precedenti impediscono efficacemente il riordino del compilatore delle operazioni atomiche su un singolo oggetto, anche se entrambe le operazioni sono carichi rilassati. Ciò rende effettivamente la garanzia di coerenza della cache fornita dalla maggior parte dell'hardware disponibile per le operazioni atomiche C ++. - nota finale]

Non esiste un meccanismo per un releasenegozio di svuotare solo se stesso e alcuni intervalli di indirizzi selezionati: dovrebbe sincronizzare tutto perché non saprebbe quali altri thread potrebbero voler leggere se il loro carico di acquisizione vedesse questo negozio di rilascio (formando un sequenza di rilascio che stabilisce una relazione accada prima tra i thread, garantendo che le operazioni non atomiche precedenti eseguite dal thread di scrittura siano ora sicure da leggere. A meno che non le scriva ulteriormente dopo il negozio di rilascio ...) O i compilatori avrebbero per essere davvero intelligente per dimostrare che solo alcune righe della cache necessitavano di svuotamento.

Correlati: la mia risposta su Mov + mfence è sicuro su NUMA? entra nel dettaglio della non esistenza di sistemi x86 senza una memoria condivisa coerente. Anche correlato: carica e archivia i riordini su ARM per ulteriori informazioni sui carichi / depositi nella stessa posizione.

Penso che ci siano cluster con memoria condivisa non coerente, ma non sono macchine a sistema singolo. Ogni dominio di coerenza esegue un kernel separato, quindi non è possibile eseguire thread di un singolo programma C ++ su di esso. Invece si eseguono istanze separate del programma (ognuna con il proprio spazio di indirizzi: i puntatori in un'istanza non sono validi nell'altra).

Per indurli a comunicare tra loro tramite flush espliciti, in genere si utilizza MPI o altre API di passaggio messaggi per fare in modo che il programma specifichi quali intervalli di indirizzi devono essere scaricati.


L'hardware reale non corre std::threadoltre i limiti di coerenza della cache:

Esistono alcuni chip ARM asimmetrici, con spazio di indirizzi fisici condiviso ma non domini di cache condivisibili all'interno. Quindi non coerente. (ad es. thread di commento un core A8 e un Cortex-M3 come TI Sitara AM335x).

Ma kernel diversi verrebbero eseguiti su quei core, non una singola immagine di sistema in grado di eseguire thread su entrambi i core. Non sono a conoscenza di implementazioni C ++ che eseguono std::threadthread tra i core della CPU senza cache coerenti.

Per ARM in particolare, GCC e clang generano codice presupponendo che tutti i thread vengano eseguiti nello stesso dominio condivisibile all'interno. In effetti, dice il manuale ISA ARMv7

Questa architettura (ARMv7) è scritta con l'aspettativa che tutti i processori che utilizzano lo stesso sistema operativo o hypervisor siano nello stesso dominio di condivisibilità condivisibile interno

Quindi la memoria condivisa non coerente tra domini separati è solo una cosa per l'uso esplicito specifico del sistema delle aree di memoria condivisa per la comunicazione tra processi diversi sotto kernel diversi.

Vedi anche questa discussione CoreCLR sul code-gen usando le barriere di memoria dmb ish(Inner Shareable barrier) vs. dmb sy(System) in quel compilatore.

Faccio l'affermazione che nessuna implementazione C ++ per altri ISA viene eseguita std::threadsu core con cache non coerenti. Non ho la prova che tale implementazione non esiste, ma sembra altamente improbabile. A meno che tu non stia prendendo di mira uno specifico pezzo esotico di HW che funziona in questo modo, il tuo pensiero sulle prestazioni dovrebbe assumere la coerenza della cache simile a MESI tra tutti i thread. (Preferibilmente usare atomic<T>in modo tale da garantire la correttezza!)


Le cache coerenti lo rendono semplice

Ma su un sistema multi-core con cache coerenti, implementare un release-store significa semplicemente ordinare il commit nella cache per i negozi di questo thread, senza eseguire alcun flush esplicito. ( https://preshing.com/20120913/acquire-and-release-semantics/ e https://preshing.com/20120710/memory-barriers-are-like-source-control-operations/ ). (E un carico di acquisizione significa ordinare l'accesso alla cache nell'altro core).

Un'istruzione di barriera della memoria blocca solo i carichi e / o gli archivi del thread corrente fino allo svuotamento del buffer di archiviazione; ciò accade sempre il più velocemente possibile da solo. ( Una barriera di memoria garantisce che la coerenza della cache sia stata completata? Risolve questo malinteso). Quindi, se non hai bisogno di ordinare, basta richiedere visibilità in altri thread, mo_relaxedva bene. (E così èvolatile , ma non farlo.)

Vedi anche mappature C / C ++ 11 ai processori

Curiosità: su x86, ogni asm store è un release-store perché il modello di memoria x86 è sostanzialmente seq-cst più un buffer di archivio (con inoltro di archivio).


Relativo ai semi: buffer di archiviazione, visibilità globale e coerenza: C ++ 11 garantisce pochissimo. La maggior parte degli ISA reali (tranne PowerPC) garantisce che tutti i thread possano concordare l'ordine di un aspetto di due negozi con altri due thread. (Nella terminologia del modello di memoria formale dell'architettura del computer, sono "multi-copia atomica").

Un altro malinteso è che le istruzioni recinzione memoria ASM sono necessarie per svuotare il buffer deposito per altri nuclei di vedere i nostri negozi a tutti . In realtà il buffer di archivio cerca sempre di svuotarsi (eseguire il commit nella cache L1d) il più velocemente possibile, altrimenti si riempirebbe e fermerebbe l'esecuzione. Ciò che fa una barriera / recinzione completa è bloccare il thread corrente fino a quando il buffer del negozio non viene svuotato , quindi i nostri carichi successivi compaiono nell'ordine globale dopo i nostri negozi precedenti.

(Il modello di memoria asm fortemente ordinato di x86 significa che volatilesu x86 potrebbe finire per darti più vicino mo_acq_rel, tranne per il fatto che il riordino in fase di compilazione con variabili non atomiche può ancora accadere. Ma la maggior parte dei non-x86 ha modelli di memoria debolmente ordinati così volatilee relaxedsono circa debole come mo_relaxedconsente.)


I commenti non sono per una discussione estesa; questa conversazione è stata spostata in chat .
Samuel Liew

2
Ottima scrittura. Questo è esattamente quello che stavo cercando (fornendo tutti i fatti) invece di un'affermazione generale che dice semplicemente "usa atomico anziché volatile per una singola bandiera booleana condivisa globale".
Bernie,

2
@bernie: l'ho scritto dopo essere stato frustrato da ripetute affermazioni secondo cui il mancato utilizzo atomicpotrebbe portare a thread diversi con valori diversi per la stessa variabile nella cache . facepalm /. Nella cache, no, nei registri CPU sì (con variabili non atomiche); Le CPU utilizzano cache coerente. Vorrei che altre domande su SO non fossero piene di spiegazioni per la atomicdiffusione di idee sbagliate su come funzionano le CPU. (Perché è una cosa utile da capire per motivi di prestazioni e aiuta anche a spiegare perché le regole atomiche ISO C ++ sono scritte così come sono.)
Peter Cordes

-1
#include <iostream>
#include <thread>
#include <unistd.h>
using namespace std;

bool checkValue = false;

int main()
{
    std::thread writer([&](){
            sleep(2);
            checkValue = true;
            std::cout << "Value of checkValue set to " << checkValue << std::endl;
        });

    std::thread reader([&](){
            while(!checkValue);
        });

    writer.join();
    reader.join();
}

Una volta un intervistatore che credeva anche che la volatilità fosse inutile sosteneva con me che l'ottimizzazione non avrebbe causato problemi e si riferiva a diversi core con linee di cache separate e tutto il resto (non capiva davvero a cosa si stesse riferendo esattamente). Ma questo pezzo di codice quando viene compilato con -O3 su g ++ (g ++ -O3 thread.cpp -lpthread), mostra un comportamento indefinito. Fondamentalmente se il valore viene impostato prima del controllo while funziona bene e in caso contrario entra in un ciclo senza preoccuparsi di recuperare il valore (che è stato effettivamente modificato dall'altro thread). Fondamentalmente credo che il valore di checkValue venga recuperato una sola volta nel registro e non venga mai ricontrollato con il massimo livello di ottimizzazione. Se è impostato su true prima del recupero, funziona bene e in caso contrario si inserisce in un ciclo. Per favore, correggimi se sbaglio.


4
Cosa c'entra questo volatile? Sì, questo codice è UB, ma è anche UB volatile.
David Schwartz,

-2

Hai bisogno di volatile e possibilmente di blocco.

volatile dice all'ottimizzatore che il valore può cambiare in modo asincrono, quindi

volatile bool flag = false;

while (!flag) {
    /*do something*/
}

leggerà la bandiera ogni volta intorno al ciclo.

Se disattivi l'ottimizzazione o rendi volatile ogni variabile, un programma si comporterà allo stesso modo ma più lentamente. volatile significa solo 'So che potresti averlo appena letto e sapere cosa dice, ma se lo dico, leggilo, quindi leggilo.

Il blocco fa parte del programma. Quindi, a proposito, se stai implementando i semafori, tra l'altro devono essere volatili. (Non provarlo, è difficile, probabilmente avrà bisogno di un piccolo assemblatore o della nuova roba atomica, ed è già stato fatto.)


1
Ma non è questo, e lo stesso esempio nell'altra risposta, un'attesa impegnata e quindi qualcosa che dovrebbe essere evitato? Se questo è un esempio inventato, ci sono esempi di vita reale che non sono inventati?
David Preston,

7
@Chris: l'attesa di tanto in tanto è una buona soluzione. In particolare, se ti aspetti di dover aspettare solo un paio di cicli di clock, comporta un carico di lavoro molto inferiore rispetto all'approccio molto più pesante di sospendere il thread. Ovviamente, come ho già detto in altri commenti, esempi come questo sono errati perché presumono che le letture / scritture sulla bandiera non saranno riordinate rispetto al codice che protegge, e tale garanzia non viene data, e quindi , volatilenon è davvero utile nemmeno in questo caso. Ma l'attesa occupata è una tecnica occasionalmente utile.
jalf

3
@richard Sì e no. La prima metà è corretta. Ma questo significa solo che CPU e compilatore non sono autorizzati a riordinare le variabili volatili l'una rispetto all'altra. Se leggo una variabile volatile A, e poi leggo una variabile volatile B, il compilatore deve emettere il codice che è garantito (anche con il riordino della CPU) per leggere A prima di B. Ma non fornisce garanzie su tutti gli accessi variabili non volatili . Possono essere riordinati attorno alla tua volatile lettura / scrittura bene. Quindi, a meno che tu non renda volatile ogni variabile del tuo programma, non ti darà la garanzia che ti interessa
jalf

2
@ ctrl-alt-delor: non è questo volatileil significato di "nessun riordino". Speri che ciò significhi che i negozi diventeranno globalmente visibili (ad altri thread) nell'ordine del programma. Questo è quello che atomic<T>con memory_order_releaseo seq_cstti dà. Ma ti dà volatile solo la garanzia di non riordinare in fase di compilazione : ogni accesso apparirà nell'asm nell'ordine del programma. Utile per un driver di dispositivo. E utile per l'interazione con un gestore di interrupt, un debugger o un gestore di segnali sul core / thread corrente, ma non per interagire con altri core.
Peter Cordes,

1
volatilein pratica è sufficiente per controllare un keep_runningflag come stai facendo qui: le CPU reali hanno sempre cache coerenti che non richiedono lo spurgo manuale. Ma non c'è alcun motivo di raccomandare volatilesopra atomic<T>con mo_relaxed; otterrai lo stesso.
Peter Cordes,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.