Domanda

Ho una classe di blocco multi-R / W che mantiene i contatori di lettura, scrittura e in attesa di lettura, in attesa di scrittura. Un mutex li protegge da più thread.

La mia domanda è: abbiamo ancora bisogno che i contatori siano dichiarati volatili in modo che il compilatore non lo rovini mentre fa l'ottimizzazione.

Oppure il compilatore tiene conto del fatto che i contatori sono sorvegliati dal mutex.

Comprendo che il mutex è un meccanismo di runtime per la sincronizzazione e "volatile" parola chiave è un'indicazione del tempo di compilazione per il compilatore di fare la cosa giusta mentre si eseguono le ottimizzazioni.

Saluti, -Jay.

È stato utile?

Soluzione

Ci sono 2 elementi sostanzialmente non correlati qui, che sono sempre confusi.

  • volatili
  • thread, blocchi, barriere di memoria, ecc.

volatile è usato per dire al compilatore di produrre codice per leggere la variabile dalla memoria, non da un registro. E per non riordinare il codice in giro. In generale, non ottimizzare o prendere "scorciatoie".

le barriere di memoria (fornite da mutex, blocchi, ecc.), citate da Herb Sutter in un'altra risposta, servono per impedire alla CPU di riordinare le richieste di memoria di lettura / scrittura, indipendentemente da come ha detto il compilatore per farlo. cioè non ottimizzare, non prendere scorciatoie a livello di CPU.

Cose simili, ma in realtà molto diverse.

Nel tuo caso, e nella maggior parte dei casi di blocco, il motivo per cui NON è necessario il volatile è dovuto al fatto che chiamate di funzione sono state fatte per motivi di blocco. vale a dire:

Chiamate di funzioni normali che influiscono sulle ottimizzazioni:

external void library_func(); // from some external library

global int x;

int f()
{
   x = 2;
   library_func();
   return x; // x is reloaded because it may have changed
}

a meno che il compilatore non possa esaminare library_func () e determinare che non tocca x, rileggerà x al ritorno. Questo è anche SENZA volatile.

Threading:

int f(SomeObject & obj)
{
   int temp1;
   int temp2;
   int temp3;

   int temp1 = obj.x;

   lock(obj.mutex); // really should use RAII
      temp2 = obj.x;
      temp3 = obj.x;
   unlock(obj.mutex);

   return temp;
}

Dopo aver letto obj.x per temp1, il compilatore rileggerà obj.x per temp2 - NON a causa della magia dei blocchi - ma perché non è sicuro se lock () abbia modificato obj. Probabilmente potresti impostare flag del compilatore per ottimizzare in modo aggressivo (no-alias, ecc.) E quindi non rileggere x, ma probabilmente un sacco di codice inizierà a fallire.

Per temp3, il compilatore (si spera) non rileggerà obj.x. Se per qualche ragione obj.x potesse cambiare tra temp2 e temp3, allora useresti volatile (e il tuo blocco sarebbe rotto / inutile).

Infine, se le funzioni lock () / unlock () fossero in qualche modo integrate, forse il compilatore potrebbe valutare il codice e vedere che obj.x non viene modificato. Ma garantisco una delle due cose qui:   - il codice inline alla fine chiama alcune funzioni di blocco a livello di sistema operativo (impedendo così la valutazione) o   - chiamate alcune istruzioni asm della barriera della memoria (ovvero che sono racchiuse in funzioni inline come __InterlockedCompareExchange) che il vostro compilatore riconoscerà e quindi eviteranno il riordino.

MODIFICA: P.S. Ho dimenticato di menzionare: per quanto riguarda pthreads, alcuni compilatori sono contrassegnati come "conformi a POSIX" il che significa, tra le altre cose, che riconosceranno le funzioni pthread_ e non faranno cattive ottimizzazioni intorno a loro. cioè anche se lo standard C ++ non menziona ancora i thread, quei compilatori lo fanno (almeno minimamente).

Quindi, risposta breve

non hai bisogno di volatile.

Altri suggerimenti

Dall'articolo di Herb Sutter "Usa le sezioni critiche (preferibilmente le serrature) per eliminare le gare" ( http://www.ddj.com/cpp/201804238 ):

  

Quindi, affinché una trasformazione riordinata sia valida, deve rispettare le sezioni critiche del programma obbedendo all'unica regola chiave delle sezioni critiche: il codice non può uscire da una sezione critica. (Va sempre bene che il codice si sposti.) Facciamo rispettare questa regola aurea richiedendo una semantica di recinzione unidirezionale simmetrica per l'inizio e la fine di qualsiasi sezione critica, illustrata dalle frecce nella Figura 1:

     
      
  • L'inserimento di una sezione critica è un'operazione di acquisizione o una recinzione di acquisizione implicita: il codice non può mai attraversare la recinzione verso l'alto, ovvero spostarsi da una posizione originale dopo la recinzione per eseguire prima della recinzione. Il codice che appare prima della barriera nell'ordine del codice sorgente, tuttavia, può felicemente attraversare la barriera verso il basso per essere eseguito in seguito.
  •   
  • L'uscita da una sezione critica è un'operazione di rilascio, o una recinzione di rilascio implicita: questo è solo il requisito inverso che il codice non possa attraversare la recinzione verso il basso, solo verso l'alto. Garantisce che qualsiasi altro thread che vede la versione finale della scrittura vedrà anche tutte le scritture precedenti.
  •   

Quindi, affinché un compilatore produca il codice corretto per una piattaforma di destinazione, quando una sezione critica viene inserita ed chiusa (e il termine sezione critica viene utilizzato in senso generico, non necessariamente nel senso Win32 di qualcosa protetto da un Struttura CRITICAL_SECTION - la sezione critica può essere protetta da altri oggetti di sincronizzazione) deve essere seguita la semantica corretta di acquisizione e rilascio. Quindi non dovresti contrassegnare le variabili condivise come volatili fintanto che sono accessibili solo all'interno di sezioni critiche protette.

volatile viene utilizzato per informare l'ottimizzatore di caricare sempre il valore corrente della posizione, anziché caricarlo in un registro e presumere che non cambierà. Questo è molto utile quando si lavora con posizioni di memoria con doppia porta o posizioni che possono essere aggiornate in tempo reale da fonti esterne al thread.

Il mutex è un meccanismo del sistema operativo di runtime di cui il compilatore non sa davvero nulla, quindi l'ottimizzatore non ne terrà conto. Impedirà a più di un thread di accedere contemporaneamente ai contatori, ma i valori di tali contatori sono ancora soggetti a modifiche anche quando il mutex è attivo.

Quindi, stai marcando le variabili volatili perché possono essere modificate esternamente e non perché si trovano all'interno di una protezione mutex.

Rendili volatili.

Sebbene ciò possa dipendere dalla libreria di threading che stai utilizzando, la mia comprensione è che qualsiasi libreria decente non richiederà l'uso di volatile .

In Pthreads, per esempio , l'uso di un mutex assicurerà che i tuoi dati vengano salvati correttamente nella memoria.

EDIT: con la presente sottoscrivo la risposta di tony come migliore della mia.

Hai ancora bisogno di " volatile " parola chiave.

I mutex impediscono ai contatori l'accesso simultaneo.

" volatili " indica al compilatore di utilizzare effettivamente il contatore invece di memorizzarlo nella cache in un registro CPU (che non lo farebbe essere aggiornato dal thread simultaneo).

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