Perché la volatile non è considerata utile nella programmazione multithread in C o C ++?


165

Come dimostrato in questa risposta che ho postato di recente, mi sembra di essere confuso riguardo all'utilità (o alla sua mancanza) volatilein contesti di programmazione multi-thread.

La mia comprensione è questa: ogni volta che una variabile può essere cambiata al di fuori del flusso di controllo di un pezzo di codice che vi accede, quella variabile dovrebbe essere dichiarata essere volatile. I gestori di segnale, i registri I / O e le variabili modificate da un altro thread costituiscono tutte queste situazioni.

Quindi, se hai un int globale foo, e fooviene letto da un thread e impostato atomicamente da un altro thread (probabilmente usando un'istruzione macchina appropriata), il thread di lettura vede questa situazione nello stesso modo in cui vede una variabile ottimizzata da un gestore di segnale o modificato da una condizione hardware esterna e quindi foodovrebbe essere dichiarato volatile(o, per situazioni multithread, accedere con un carico recintato in memoria, che probabilmente è una soluzione migliore).

Come e dove sbaglio?


7
Tutto ciò che è volatile è dire che il compilatore non dovrebbe memorizzare nella cache l'accesso a una variabile volatile. Non dice nulla sulla serializzazione di tale accesso. Questo è stato discusso qui Non so quante volte, e non credo che questa domanda aggiungerà nulla a quelle discussioni.

4
E ancora una volta, una domanda che non lo merita ed è stata posta qui molte volte prima che venga votata. Per favore, smetti di farlo.

14
@neil Ho cercato altre domande e ne ho trovata una, ma qualsiasi spiegazione esistente che ho visto in qualche modo non ha innescato ciò di cui avevo bisogno per capire davvero perché mi sbagliavo. Questa domanda ha suscitato una simile risposta.
Michael Ekstrand,

1
Per un ottimo studio approfondito su ciò che le CPU fanno con i dati (tramite le loro cache) consulta: rdrop.com/users/paulmck/scalability/paper/whymb.2010.06.07c.pdf
Sassafras_wot

1
@curiousguy Questo è ciò che intendevo con "non è il caso in C", dove può essere usato per scrivere su registri hardware ecc., e non è usato per il multithreading come è comunemente usato in Java.
Monstieur,

Risposte:


213

Il problema volatilein un contesto multithread è che non fornisce tutte le garanzie di cui abbiamo bisogno. Ha alcune proprietà di cui abbiamo bisogno, ma non tutte, quindi non possiamo fare affidamento da volatile sole .

Tuttavia, le primitive che dovremmo usare per le restanti proprietà forniscono anche quelle che lo volatilefanno, quindi è effettivamente superfluo.

Per gli accessi thread-safe ai dati condivisi, abbiamo bisogno di una garanzia che:

  • la lettura / scrittura avviene effettivamente (che il compilatore non memorizzerà semplicemente il valore in un registro e rinvierà l'aggiornamento della memoria principale fino a molto tempo dopo)
  • che non ha luogo alcun riordino. Supponiamo che utilizziamo una volatilevariabile come flag per indicare se alcuni dati sono pronti per la lettura. Nel nostro codice, abbiamo semplicemente impostato il flag dopo aver preparato i dati, quindi tutto sembra a posto. Ma cosa succede se le istruzioni vengono riordinate in modo che la bandiera venga impostata per prima ?

volatilegarantisce il primo punto. Garantisce inoltre che non si verifichi alcun riordino tra diverse letture / scritture volatili . Tutti gli volatileaccessi alla memoria avverranno nell'ordine in cui sono specificati. Questo è tutto ciò di cui abbiamo bisogno per ciò che volatileè destinato: manipolare i registri I / O o l'hardware mappato in memoria, ma non ci aiuta nel codice multithread in cui l' volatileoggetto viene spesso utilizzato solo per sincronizzare l'accesso a dati non volatili. Tali accessi possono ancora essere riordinati rispetto a volatilequelli.

La soluzione per prevenire il riordino è utilizzare una barriera di memoria , che indica sia al compilatore che alla CPU che non è possibile riordinare l'accesso alla memoria attraverso questo punto . Posizionare tali barriere attorno al nostro accesso variabile volatile assicura che anche gli accessi non volatili non vengano riordinati su quello volatile, permettendoci di scrivere un codice thread-safe.

Tuttavia, le barriere di memoria assicurano anche che tutte le letture / scritture in sospeso vengano eseguite quando viene raggiunta la barriera, quindi ci dà effettivamente tutto ciò di cui abbiamo bisogno da soli, rendendo volatilesuperfluo. Possiamo semplicemente rimuovere del volatiletutto il qualificatore.

Dal C ++ 11, le variabili atomiche ( std::atomic<T>) ci offrono tutte le garanzie pertinenti.


5
@jbcreix: Di quale "si" stai chiedendo? Barriere volatili o di memoria? In ogni caso, la risposta è praticamente la stessa. Entrambi devono funzionare sia a livello di compilatore che a livello di CPU, poiché descrivono il comportamento osservabile del programma --- quindi devono assicurarsi che la CPU non riordini tutto, modificando il comportamento che garantiscono. Ma al momento non puoi scrivere la sincronizzazione di thread portatili, perché le barriere di memoria non fanno parte del C ++ standard (quindi non sono portatili) e volatilenon sono abbastanza forti per essere utili.
jalf

4
Un esempio MSDN fa questo, e sostiene che le istruzioni non possono essere riordinate passato un accesso volatile: msdn.microsoft.com/en-us/library/12a04hfd(v=vs.80).aspx
OJW

27
@OJW: Ma il compilatore di Microsoft ridefinisce volatileper essere una barriera di memoria completa (impedendo il riordino). Non fa parte dello standard, quindi non puoi fare affidamento su questo comportamento nel codice portatile.
jalf

4
@Skizz: no, è qui che entra in gioco la parte "magia del compilatore" dell'equazione. Una barriera di memoria deve essere compresa sia dalla CPU che dal compilatore. Se il compilatore comprende la semantica di una barriera di memoria, sa evitare trucchi del genere (oltre a riordinare letture / scritture attraverso la barriera). E per fortuna, il compilatore fa comprendere la semantica di una barriera di memoria, quindi alla fine, tutto funziona. :)
jalf

13
@Skizz: i thread stessi sono sempre un'estensione dipendente dalla piattaforma prima di C ++ 11 e C11. Per quanto ne so, ogni ambiente C e C ++ che fornisce un'estensione threading fornisce anche un'estensione "barriera di memoria". Indipendentemente da ciò, volatileè sempre inutile per la programmazione multi-thread. (Tranne che in Visual Studio, dove volatile è l'estensione della barriera di memoria.)
Nemo

49

Si potrebbe anche considerare questo dalla documentazione del kernel Linux .

I programmatori C sono spesso diventati volatili per indicare che la variabile potrebbe essere cambiata al di fuori dell'attuale thread di esecuzione; di conseguenza, a volte sono tentati di usarlo nel codice del kernel quando vengono utilizzate strutture di dati condivise. In altre parole, sono noti per trattare i tipi volatili come una sorta di facile variabile atomica, cosa che non lo sono. L'uso di volatile nel codice del kernel non è quasi mai corretto; questo documento descrive il perché.

Il punto chiave da capire riguardo alla volatile è che il suo scopo è sopprimere l'ottimizzazione, che non è quasi mai ciò che si vuole veramente fare. Nel kernel, è necessario proteggere le strutture di dati condivise dall'accesso simultaneo indesiderato, che è un compito molto diverso. Il processo di protezione contro la concorrenza indesiderata eviterà inoltre quasi tutti i problemi relativi all'ottimizzazione in modo più efficiente.

Come volatili, le primitive del kernel che rendono sicuro l'accesso simultaneo ai dati (spinlock, mutex, barriere di memoria, ecc.) Sono progettate per prevenire l'ottimizzazione indesiderata. Se vengono utilizzati correttamente, non sarà necessario utilizzare anche volatile. Se volatile è ancora necessario, c'è quasi sicuramente un bug nel codice da qualche parte. Nel codice del kernel scritto correttamente, volatile può servire solo a rallentare le cose.

Considera un tipico blocco di codice kernel:

spin_lock(&the_lock);
do_something_on(&shared_data);
do_something_else_with(&shared_data);
spin_unlock(&the_lock);

Se tutto il codice segue le regole di blocco, il valore di shared_data non può cambiare in modo imprevisto mentre the_lock viene mantenuto. Qualsiasi altro codice che potrebbe voler giocare con quei dati sarà in attesa sul lucchetto. Le primitive spinlock agiscono come barriere di memoria - sono esplicitamente scritte per farlo - il che significa che gli accessi ai dati non saranno ottimizzati su di loro. Quindi il compilatore potrebbe pensare di sapere cosa sarà in shared_data, ma la chiamata spin_lock (), dal momento che agisce come una barriera di memoria, lo costringerà a dimenticare tutto ciò che sa. Non ci saranno problemi di ottimizzazione con l'accesso a tali dati.

Se shared_data fosse dichiarato volatile, il blocco sarebbe comunque necessario. Ma al compilatore verrebbe anche impedito di ottimizzare l'accesso a shared_data all'interno della sezione critica, quando sappiamo che nessun altro può lavorarci. Mentre il blocco viene mantenuto, shared_data non è volatile. Quando si ha a che fare con dati condivisi, il blocco adeguato rende superflua la volatilità e potenzialmente dannosa.

La classe di archiviazione volatile era originariamente pensata per i registri I / O associati alla memoria. All'interno del kernel, anche gli accessi al registro dovrebbero essere protetti da blocchi, ma non si vuole che il compilatore "ottimizzi" gli accessi al registro all'interno di una sezione critica. Ma, all'interno del kernel, gli accessi alla memoria I / O vengono sempre eseguiti tramite le funzioni accessor; l'accesso alla memoria I / O direttamente attraverso i puntatori è mal visto e non funziona su tutte le architetture. Questi accessor sono scritti per prevenire l'ottimizzazione indesiderata, quindi, ancora una volta, la volatilità non è necessaria.

Un'altra situazione in cui si potrebbe essere tentati di usare volatile è quando il processore è impegnato ad aspettare il valore di una variabile. Il modo giusto per eseguire un'attesa impegnativa è:

while (my_variable != what_i_want)
    cpu_relax();

La chiamata cpu_relax () può ridurre il consumo di energia della CPU o cedere a un doppio processore hyperthreaded; capita anche che funga da barriera di memoria, quindi, ancora una volta, la volatilità non è necessaria. Certo, l'attesa è generalmente un atto antisociale per cominciare.

Ci sono ancora alcune rare situazioni in cui volatile ha senso nel kernel:

  • Le funzioni di accesso sopra menzionate potrebbero utilizzare volatili su architetture in cui funziona l'accesso diretto alla memoria I / O. In sostanza, ogni chiamata all'accessorio diventa una piccola sezione critica e garantisce che l'accesso avvenga come previsto dal programmatore.

  • Codice assembly incorporato che modifica la memoria, ma che non ha altri effetti collaterali visibili, rischia di essere eliminato da GCC. L'aggiunta della parola chiave volatile alle istruzioni asm impedirà questa rimozione.

  • La variabile jiffies è speciale in quanto può avere un valore diverso ogni volta che viene referenziata, ma può essere letta senza alcun blocco speciale. Quindi i jiffies possono essere volatili, ma l'aggiunta di altre variabili di questo tipo è fortemente disapprovata. Jiffies è considerato un problema di "stupido retaggio" (le parole di Linus) al riguardo; risolverlo sarebbe più un problema di quanto valga la pena.

  • I puntatori a strutture di dati nella memoria coerente che potrebbero essere modificati da dispositivi I / O possono, a volte, essere legittimamente volatili. Un buffer ad anello utilizzato da una scheda di rete, in cui tale scheda cambia i puntatori per indicare quali descrittori sono stati elaborati, è un esempio di questo tipo di situazione.

Per la maggior parte del codice, non si applica nessuna delle giustificazioni di cui sopra per volatile. Di conseguenza, è probabile che l'uso di volatile sia visto come un bug e porterà ulteriore controllo al codice. Gli sviluppatori che sono tentati di usare la volatilità dovrebbero fare un passo indietro e pensare a ciò che stanno veramente cercando di realizzare.



1
Spin_lock () sembra una normale chiamata di funzione. La particolarità di questo è che il compilatore lo tratterà in modo speciale in modo che il codice generato "dimentichi" qualsiasi valore di shared_data che è stato letto prima di spin_lock () e memorizzato in un registro in modo che il valore debba essere letto nuovamente nel do_something_on () dopo spin_lock ()?
Sincronizzato il

1
@underscore_d Il mio punto è che non posso dire dal nome della funzione spin_lock () che fa qualcosa di speciale. Non so cosa ci sia dentro. In particolare, non so cosa ci sia nell'implementazione che impedisce al compilatore di ottimizzare le letture successive.
Sincronizzato il

1
Syncopated ha un buon punto. Ciò significa essenzialmente che il programmatore dovrebbe conoscere l'implementazione interna di quelle "funzioni speciali" o almeno essere ben informato sul loro comportamento. Ciò solleva ulteriori domande, come ad esempio: queste funzioni speciali sono standardizzate e garantite per funzionare allo stesso modo su tutte le architetture e tutti i compilatori? È disponibile un elenco di tali funzioni o almeno esiste una convenzione per utilizzare i commenti sul codice per segnalare agli sviluppatori che la funzione in questione protegge il codice dall'essere "ottimizzato"?
JustAMartin,

1
@Tuntable: una statica privata può essere toccata da qualsiasi codice, tramite un puntatore. E il suo indirizzo è stato preso. Forse l'analisi del flusso di dati è in grado di dimostrare che il puntatore non sfugge mai, ma che in generale è un problema molto difficile, superlineare nelle dimensioni del programma. Se hai un modo per garantire che non esistano alias, lo spostamento dell'accesso attraverso un blocco di spin dovrebbe effettivamente essere corretto. Ma se non esistono alias, volatileè anche inutile. In tutti i casi, il comportamento "chiamata a una funzione il cui corpo non può essere visto" sarà corretto.
Ben Voigt,

11

Non credo che tu abbia torto: volatile è necessario per garantire che il thread A vedrà il valore cambiare, se il valore è cambiato da qualcosa di diverso dal thread A. A quanto ho capito, volatile è fondamentalmente un modo per dire al compilatore "non memorizzare nella cache questa variabile in un registro, ma assicurati sempre di leggerla / scriverla dalla memoria RAM ad ogni accesso".

La confusione è perché la volatilità non è sufficiente per implementare una serie di cose. In particolare, i sistemi moderni utilizzano più livelli di memorizzazione nella cache, le moderne CPU multi-core eseguono alcune ottimizzazioni fantasiose in fase di esecuzione e i compilatori moderni eseguono alcune ottimizzazioni fantasiose in fase di compilazione, e tutto ciò può comportare la comparsa di vari effetti collaterali in modo diverso ordine dall'ordine che ti aspetteresti se solo guardassi il codice sorgente.

Così volatile va bene, purché si tenga presente che i cambiamenti "osservati" nella variabile volatile potrebbero non verificarsi nel momento esatto in cui si pensa che lo faranno. In particolare, non tentare di utilizzare variabili volatili come modo per sincronizzare o ordinare operazioni tra thread, perché non funzionerà in modo affidabile.

Personalmente, il mio principale (solo?) Uso per la bandiera volatile è come un booleano "pleaseGoAwayNow". Se ho un thread di lavoro che gira continuamente, lo farò controllare il valore booleano volatile su ogni iterazione del ciclo e uscire se il valore booleano è sempre vero. Il thread principale può quindi ripulire in modo sicuro il thread di lavoro impostando il valore booleano su true e quindi chiamando pthread_join () per attendere fino a quando il thread di lavoro non viene rimosso.


2
La tua bandiera booleana non è probabilmente sicura. Come garantite che il lavoratore completi il ​​suo compito e che la bandiera rimanga nell'ambito finché non viene letta (se viene letta)? Questo è un lavoro per i segnali. Volatile è utile per implementare semplici spinlock se non è coinvolto il mutex, poiché alias safety indica che il compilatore assume mutex_lock(e ogni altra funzione di libreria) può alterare lo stato della variabile flag.
Potatoswatter

6
Ovviamente funziona solo se la natura della routine del thread di lavoro è tale da garantire periodicamente il controllo del booleano. Il flag volatile-bool è garantito per rimanere nell'ambito poiché la sequenza di arresto del thread si verifica sempre prima che l'oggetto che contiene il volatile-booleano venga distrutto e la sequenza di arresto del thread chiama pthread_join () dopo aver impostato il valore booleano. pthread_join () si bloccherà fino a quando il thread di lavoro non sarà andato via. I segnali hanno i loro problemi, in particolare se usati insieme al multithreading.
Jeremy Friesner,

2
Il thread di lavoro non è garantito per completare il suo lavoro prima che il valore booleano sia vero - in effetti, quasi certamente sarà nel mezzo di un'unità di lavoro quando il valore booleano è impostato su vero. Ma non importa quando il thread di lavoro completa la sua unità di lavoro, perché il thread principale non farà altro che bloccare all'interno di pthread_join () fino a quando il thread di lavoro non esce, in ogni caso. Quindi la sequenza di arresto è ben ordinata: il bool volatile (e tutti gli altri dati condivisi) non saranno liberati fino a quando non verrà restituito pthread_join () e pthread_join () non tornerà fino a quando il thread di lavoro non sarà andato.
Jeremy Friesner,

10
@Jeremy, in pratica hai ragione ma teoricamente potrebbe ancora rompersi. Su un sistema a due core, un core esegue costantemente il thread di lavoro. L'altro core imposta il bool su true. Tuttavia, non vi è alcuna garanzia che il nucleo del thread di lavoro possa mai vedere quel cambiamento, vale a dire che potrebbe non fermarsi mai anche se ha ripetuto il controllo del bool. Questo comportamento è consentito dai modelli di memoria c ++ 0x, java e c #. In pratica ciò non si verificherebbe mai poiché il thread occupato molto probabilmente inserisce una barriera di memoria da qualche parte, dopo di che vedrà la modifica al bool.
deft_code

4
Prendi un sistema POSIX, usa una politica di pianificazione in tempo reale SCHED_FIFO, una priorità statica superiore rispetto ad altri processi / thread nel sistema, abbastanza core, dovrebbe essere perfettamente possibile. In Linux è possibile specificare che il processo in tempo reale può utilizzare il 100% del tempo della CPU. Non cambieranno mai contesto se non ci sono thread / processi con priorità più elevata e non si bloccheranno mai per I / O. Ma il punto è che C / C ++ volatilenon è pensato per far rispettare la semantica di condivisione / sincronizzazione dei dati. Trovo che cercare casi speciali per dimostrare che un codice errato a volte potrebbe funzionare sia un esercizio inutile.
FooF

7

volatile è utile (anche se insufficiente) per implementare il costrutto di base di un mutex spinlock, ma una volta che lo hai (o qualcosa di superiore), non hai bisogno di un altro volatile .

Il modo tipico della programmazione multithread non è proteggere ogni variabile condivisa a livello di macchina, ma piuttosto introdurre variabili di protezione che guidano il flusso del programma. Invece di volatile bool my_shared_flag;te dovresti

pthread_mutex_t flag_guard_mutex; // contains something volatile
bool my_shared_flag;

Questo non solo incapsula la "parte difficile", ma è fondamentalmente necessario: C non include le operazioni atomiche necessarie per implementare un mutex; deve solo volatilefornire garanzie extra sull'ordinario operazioni .

Ora hai qualcosa del genere:

pthread_mutex_lock( &flag_guard_mutex );
my_local_state = my_shared_flag; // critical section
pthread_mutex_unlock( &flag_guard_mutex );

pthread_mutex_lock( &flag_guard_mutex ); // may alter my_shared_flag
my_shared_flag = ! my_shared_flag; // critical section
pthread_mutex_unlock( &flag_guard_mutex );

my_shared_flag non deve essere volatile, nonostante sia invariabile, perché

  1. Un altro thread ha accesso ad esso.
  2. Significa che un riferimento ad esso deve essere stato preso qualche volta (con il & operatore).
    • (O è stato preso un riferimento a una struttura di contenimento)
  3. pthread_mutex_lock è una funzione di libreria.
  4. Significa che il compilatore non può dire se in pthread_mutex_lockqualche modo acquisisce quel riferimento.
  5. Significa che il compilatore deve assumere che pthread_mutex_lockmodifica il flag condiviso !
  6. Quindi la variabile deve essere ricaricata dalla memoria. volatile, sebbene significativo in questo contesto, è estraneo.

6

La tua comprensione è davvero sbagliata.

La proprietà, che hanno le variabili volatili, è "legge e scrive su questa variabile fa parte del comportamento percepibile del programma". Ciò significa che questo programma funziona (dato l'hardware appropriato):

int volatile* reg=IO_MAPPED_REGISTER_ADDRESS;
*reg=1; // turn the fuel on
*reg=2; // ignition
*reg=3; // release
int x=*reg; // fire missiles

Il problema è che questa non è la proprietà che desideriamo da un thread sicuro.

Ad esempio, un contatore thread-safe sarebbe giusto (codice simile a kernel Linux, non conosco l'equivalente c ++ 0x):

atomic_t counter;

...
atomic_inc(&counter);

Questo è atomico, senza una barriera di memoria. Dovresti aggiungerli se necessario. L'aggiunta di volatili probabilmente non sarebbe di aiuto, perché non metterebbe in relazione l'accesso al codice vicino (ad es. All'aggiunta di un elemento all'elenco che sta contando il contatore). Certamente, non è necessario vedere il contatore incrementato al di fuori del programma e le ottimizzazioni sono comunque desiderabili, ad es.

atomic_inc(&counter);
atomic_inc(&counter);

può ancora essere ottimizzato per

atomically {
  counter+=2;
}

se l'ottimizzatore è abbastanza intelligente (non cambia la semantica del codice).


6

Perché i tuoi dati siano coerenti in un ambiente concorrente, devi applicare due condizioni:

1) Atomicità, ovvero se leggo o scrivo alcuni dati in memoria, i dati vengono letti / scritti in un passaggio e non possono essere interrotti o contesi a causa, ad esempio, di un cambio di contesto

2) Coerenza, cioè l'ordine delle operazioni di lettura / scrittura deve essere visto come lo stesso tra più ambienti simultanei - che siano thread, macchine ecc.

volatile non si adatta a nessuno dei precedenti - o più in particolare, lo standard c o c ++ su come dovrebbe comportarsi il volatile non include nessuno dei precedenti.

È ancora peggio in pratica poiché alcuni compilatori (come il compilatore Intel Itanium) tentano di implementare alcuni elementi del comportamento sicuro di accesso simultaneo (cioè garantendo recinzioni di memoria), tuttavia non vi è coerenza tra le implementazioni del compilatore e inoltre lo standard non lo richiede dell'attuazione in primo luogo.

Contrassegnare una variabile come volatile significa solo che stai forzando il valore da scaricare nella memoria e dalla memoria ogni volta che in molti casi rallenta il tuo codice poiché hai sostanzialmente ridotto le prestazioni della cache.

c # e java AFAIK risolvono il problema facendo aderire volatile a 1) e 2), tuttavia lo stesso non si può dire per i compilatori c / c ++, quindi fondamentalmente fare ciò che si ritiene opportuno.

Per qualche discussione più approfondita (anche se non imparziale) sull'argomento leggi questo


3
+1 - l'atomicità garantita era un altro pezzo di ciò che mi mancava. Supponevo che il caricamento di un int fosse atomico, quindi che volatile impedendo il riordino forniva la soluzione completa sul lato letto. Penso che sia un presupposto decente per la maggior parte delle architetture, ma non è una garanzia.
Michael Ekstrand,

Quando le letture e le scritture individuali nella memoria sono interrompibili e non atomiche? C'è qualche vantaggio?
Batbrat,

5

Le FAQ di comp.programming.threads hanno una spiegazione classica di Dave Butenhof:

D56: Perché non è necessario dichiarare VOLATILE le variabili condivise?

Sono preoccupato, tuttavia, per i casi in cui sia il compilatore che la libreria dei thread soddisfano le rispettive specifiche. Un compilatore C conforme può allocare globalmente una variabile condivisa (non volatile) a un registro che viene salvato e ripristinato quando la CPU viene passata da un thread all'altro. Ogni thread avrà il proprio valore privato per questa variabile condivisa, che non è quello che vogliamo da una variabile condivisa.

In un certo senso questo è vero, se il compilatore conosce abbastanza i rispettivi ambiti della variabile e le funzioni pthread_cond_wait (o pthread_mutex_lock). In pratica, la maggior parte dei compilatori non proverà a conservare copie del registro di dati globali attraverso una chiamata a una funzione esterna, perché è troppo difficile sapere se la routine potrebbe in qualche modo avere accesso all'indirizzo dei dati.

Quindi sì, è vero che un compilatore che è strettamente (ma molto aggressivo) ANSI C potrebbe non funzionare con più thread senza volatile. Ma qualcuno dovrebbe ripararlo. Perché qualsiasi SISTEMA (ovvero, pragmaticamente, una combinazione di kernel, librerie e compilatore C) che non fornisce le garanzie di coerenza della memoria POSIX non CONFORME allo standard POSIX. Periodo. Il sistema NON PU require richiedere l'uso volatile su variabili condivise per un comportamento corretto, poiché POSIX richiede solo che siano necessarie le funzioni di sincronizzazione POSIX.

Quindi se il tuo programma si interrompe perché non hai usato volatile, questo è un ERRORE. Potrebbe non essere un bug in C, un bug nella libreria dei thread o un bug nel kernel. Ma è un bug di SISTEMA e uno o più di questi componenti dovranno lavorare per risolverlo.

Non vuoi usare volatile, perché, su qualsiasi sistema in cui fa la differenza, sarà molto più costoso di una variabile non volatile corretta. (ANSI C richiede "punti di sequenza" per le variabili volatili su ogni espressione, mentre POSIX le richiede solo durante le operazioni di sincronizzazione - un'applicazione filettata ad alta intensità di calcolo vedrà sostanzialmente più attività di memoria usando volatile e, dopotutto, è l'attività di memoria che ti rallenta davvero.)

/ --- [Dave Butenhof] ----------------------- [butenhof@zko.dec.com] --- \
| Digital Equipment Corporation 110 Spit Brook Rd ZKO2-3 / Q18 |
| 603.881.2218, FAX 603.881.0120 Nashua NH 03062-2698 |
----------------- [Vivere meglio attraverso la concorrenza] ---------------- /

Il sig. Butenhof copre gran parte dello stesso motivo in questo post usenet :

L'uso di "volatile" non è sufficiente per garantire la corretta visibilità della memoria o la sincronizzazione tra i thread. L'uso di un mutex è sufficiente e, salvo ricorrere a varie alternative di codice macchina non portatile ((o implicazioni più sottili delle regole di memoria POSIX che sono molto più difficili da applicare in generale, come spiegato nel mio precedente post), un mutex è NECESSARIO.

Pertanto, come ha spiegato Bryan, l'uso di volatile non fa altro che impedire al compilatore di apportare ottimizzazioni utili e desiderabili, non fornendo alcun aiuto nel rendere il codice "thread sicuro". Ovviamente, sei il benvenuto a dichiarare "volatile" tutto ciò che desideri: dopo tutto è un attributo di archiviazione ANSI C. Non aspettarti che risolva i problemi di sincronizzazione dei thread per te.

Tutto ciò è ugualmente applicabile al C ++.


Il collegamento è interrotto; non sembra più indicare ciò che volevi citare. Senza il testo, è una specie di risposta insignificante.
jww

3

Questo è tutto ciò che "volatile" sta facendo: "Ehi compilatore, questa variabile potrebbe cambiare IN QUALUNQUE MOMENTO (su ogni tick di clock) anche se NON ci sono ISTRUZIONI LOCALI che agiscono su di essa. NON memorizzare questo valore in un registro".

Questo è IT. Indica al compilatore che il tuo valore è, beh, volatile: questo valore può essere modificato in qualsiasi momento da una logica esterna (un altro thread, un altro processo, il Kernel, ecc.). Esiste più o meno esclusivamente per sopprimere le ottimizzazioni del compilatore che memorizzeranno in silenzio un valore in un registro che è intrinsecamente pericoloso per la cache MAI.

È possibile che si verifichino articoli come "Dr. Dobbs" che risultano instabili come una panacea per la programmazione multi-thread. Il suo approccio non è totalmente privo di merito, ma ha il difetto fondamentale di rendere gli utenti di un oggetto responsabili della sicurezza del thread, che tende ad avere gli stessi problemi di altre violazioni dell'incapsulamento.


3

Secondo il mio vecchio standard C, "Ciò che costituisce un accesso a un oggetto che ha un tipo volatile qualificato è definito dall'implementazione" . Quindi gli autori di compilatori C avrebbero potuto scegliere di avere "volatile" che significa "accesso sicuro ai thread in un ambiente multi-processo" . Ma non lo fecero.

Invece, le operazioni necessarie per rendere sicuro un thread di sezione critica in un ambiente di memoria condivisa multi-core multi-processo sono state aggiunte come nuove funzionalità definite dall'implementazione. E, liberati dal requisito secondo cui "volatile" fornirebbe l'accesso atomico e l'ordinamento degli accessi in un ambiente multi-processo, gli autori del compilatore hanno dato la priorità alla riduzione del codice rispetto alla semantica "volatile" dipendente dall'implementazione storica.

Ciò significa che cose come i semafori "volatili" attorno alle sezioni di codice critico, che non funzionano su nuovo hardware con nuovi compilatori, potrebbero aver funzionato una volta con compilatori vecchi su hardware vecchio e alcuni vecchi esempi a volte non sono sbagliati, solo vecchi.


I vecchi esempi richiedevano che il programma fosse elaborato da compilatori di qualità adatti alla programmazione di basso livello. Sfortunatamente, i compilatori "moderni" hanno preso il fatto che lo Standard non richiede loro di elaborare "volatile" in modo utile come indicazione che il codice che richiederebbe loro di farlo è rotto, piuttosto che riconoscere che lo Standard non fa lo sforzo di vietare implementazioni conformi ma di qualità così bassa da risultare inutili, ma non condanna in alcun modo compilatori di bassa qualità ma conformi che sono diventati popolari
supercat

Sulla maggior parte delle piattaforme, sarebbe abbastanza facile riconoscere cosa volatilesarebbe necessario fare per consentire a uno di scrivere un sistema operativo in modo dipendente dall'hardware ma indipendente dal compilatore. Il fatto che i programmatori utilizzino le funzionalità dipendenti dall'implementazione piuttosto che fare il volatilelavoro come richiesto mina lo scopo di avere uno standard.
supercat,
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.