2011-09-18 10 views
26

Ho provato a cercare dettagli su questo, ho persino letto lo standard su mutex e atomics ... ma non riuscivo a capire le garanzie di visibilità del modello di memoria C++ 11. Da quello che ho capito la caratteristica molto importante del Mutex, l'esclusione reciproca sta assicurando visibilità. Aka non è sufficiente che solo un thread per volta stia aumentando il contatore, è importante che il thread aumenti il ​​contatore che è stato memorizzato dal thread che era l'ultimo usando il mutex (non so davvero perché le persone non lo menzionino di più quando discutono mutex, forse ho avuto cattivi insegnanti :)). Quindi, da quello che posso dire doesnt atomica rispettare visibilità immediata: (da parte della persona che mantiene boost :: filo e ha implementato C++ 11 filo e biblioteca mutex):Ordinamento e visibilità del modello di memoria?

una recinzione con memory_order_seq_cst non impone immediata visibilità ad altri thread (e nemmeno un'istruzione MFENCE). I vincoli di ordinamento della memoria C++ 0x sono proprio quello --- che ordina i vincoli . Le operazioni memory_order_seq_cst formano un ordine totale, ma non ci sono restrizioni su cosa sia quell'ordine, eccetto che deve essere concordato su tutti i thread, e non deve violare altri vincoli di ordinazione. In particolare, i thread potrebbero continuare a visualizzare valori "stantio" per un po 'di tempo, a condizione che visualizzino i valori in un ordine coerente con i vincoli .

E io sono OK con quello. Ma il problema è che ho difficoltà a capire quali sono i costrutti C++ 11 per quanto riguarda l'atomico sono "globali" e che assicurano solo la coerenza delle variabili atomiche. In particolare ho la comprensione che (se esiste) dei seguenti memory ordinamenti garantiscono che ci sarà una recinzione di memoria prima e dopo il carico e lo memorizza: http://www.stdthread.co.uk/doc/headers/atomic/memory_order.html

Da quello che posso dire std :: memory_order_seq_cst inserisce barriera mem mentre altri applicano solo l'ordine delle operazioni su determinate posizioni di memoria.

Così qualcuno può chiarire la situazione, presumo un sacco di persone stanno andando essere fare errori orribili utilizzando std :: atomica, specialmente se essi non utilizzare di default (std :: memory_order_seq_cst memoria ordinazione)
2. se io' m destra vuol dire che la seconda linea è redundand in questo codice:

atomicVar.store(42); 
std::atomic_thread_fence(std::memory_order_seq_cst); 

3. fare std :: atomic_thread_fences hanno stessi requisiti mutex in un senso che per assicurare la coerenza seq su nonatomic vars bisogna fare std :: atomic_thread_fence (std :: memory_order_seq_cst); prima del caricamento e std :: atomic_thread_fence (std :: memory_order_seq_cst);
dopo i negozi?
4.

{ 
    regularSum+=atomicVar.load(); 
    regularVar1++; 
    regularVar2++; 
    } 
    //... 
    { 
    regularVar1++; 
    regularVar2++; 
    atomicVar.store(74656); 
    } 

equivalente a

std::mutex mtx; 
{ 
    std::unique_lock<std::mutex> ul(mtx); 
    sum+=nowRegularVar; 
    regularVar++; 
    regularVar2++; 
} 
//.. 
{ 
    std::unique_lock<std::mutex> ul(mtx); 
    regularVar1++; 
    regularVar2++; 
    nowRegularVar=(74656); 
} 

Credo di no, ma vorrei essere sicuro.

MODIFICA: 5. Può affermare il fuoco?
Esistono solo due thread.

atomic<int*> p=nullptr; 

primo thread scrive

{ 
    nonatomic_p=(int*) malloc(16*1024*sizeof(int)); 
    for(int i=0;i<16*1024;++i) 
    nonatomic_p[i]=42; 
    p=nonatomic; 
} 

secondo thread legge

{ 
    while (p==nullptr) 
    { 
    } 
    assert(p[1234]==42);//1234-random idx in array 
} 

risposta

22

Se vi piace a che fare con le recinzioni, poi a.load(memory_order_acquire) è equivalente a a.load(memory_order_relaxed) seguito da atomic_thread_fence(memory_order_acquire). Allo stesso modo, a.store(x,memory_order_release) equivale a una chiamata a atomic_thread_fence(memory_order_release) prima di una chiamata a a.store(x,memory_order_relaxed). memory_order_consume è un caso speciale di memory_order_acquire, per dati dipendenti solo. memory_order_seq_cst è speciale e forma un ordine totale per tutte le operazioni memory_order_seq_cst. Misto con gli altri è lo stesso di un acquisto per un carico e un rilascio per un negozio. memory_order_acq_rel è per operazioni di lettura-modifica-scrittura ed equivale ad una acquisizione sulla parte letta e una versione sulla parte di scrittura di RMW.

L'utilizzo dei vincoli di ordine sulle operazioni atomiche può o non può portare a istruzioni di recinzione effettive, a seconda dell'architettura hardware. In alcuni casi, il compilatore genererà un codice migliore se si imposta il vincolo di ordinamento sull'operazione atomica anziché utilizzare una fence separata.

Su x86, i carichi vengono sempre acquisiti e i negozi sono sempre rilasciati. memory_order_seq_cst richiede un ordine più forte con un'istruzione MFENCE o un'istruzione con prefisso LOCK (è disponibile qui una scelta di implementazione per stabilire se il negozio ha l'ordine o il carico più forte). Di conseguenza, le reti fisse di acquisizione e rilascio sono no-op, ma non lo è il numero atomic_thread_fence(memory_order_seq_cst) (che richiede nuovamente un'istruzione MFENCE o LOCK ed).

Un importante effetto dei vincoli di ordine è che ordinano le operazioni .

std::atomic<bool> ready(false); 
int i=0; 

void thread_1() 
{ 
    i=42; 
    ready.store(true,memory_order_release); 
} 

void thread_2() 
{ 
    while(!ready.load(memory_order_acquire)) std::this_thread::yield(); 
    assert(i==42); 
} 

thread_2 giri fino a quando si legge true da ready.Poiché il negozio per ready in thread_1 è un rilascio e il carico è un'acquisizione poi il negozio sincronizza-con il carico, e il negozio per iaccade-prima il carico dalla i nella affermare, e l'asserzione sarà non fuoco

2) La seconda riga

atomicVar.store(42); 
std::atomic_thread_fence(std::memory_order_seq_cst); 

è infatti potenzialmente ridondante, poiché l'archivio di atomicVar utilizza memory_order_seq_cst di default. Tuttavia, se ci sono altre operazioni atomiche non memory_order_seq_cst su questa discussione, la recinzione potrebbe avere delle conseguenze. Ad esempio, agirà come una barriera di rilascio per un successivo a.store(x,memory_order_relaxed).

3) Le recinzioni e le operazioni atomiche non funzionano come mutex. Puoi usarli per costruire mutex, ma non funzionano come loro. Non è necessario utilizzare mai lo atomic_thread_fence(memory_order_seq_cst). Non è necessario che le operazioni atomiche siano memory_order_seq_cst e che l'ordine su variabili non atomiche possa essere ottenuto senza, come nell'esempio sopra.

4) No questi non sono equivalenti. Il tuo frammento senza il blocco mutex è quindi una corsa di dati e un comportamento indefinito.

5) No, la tua affermazione non può sparare. Con l'ordine di memoria predefinito di memory_order_seq_cst, il negozio e il caricamento dal puntatore atomico p funzionano come il negozio e caricano nel mio esempio sopra e si garantisce che i negozi agli elementi dell'array avvengano prima delle letture.

+0

quindi in 5) per (int i = 0; i <16 * 1024; ++ i) nonatomic_p [i] = 42; non può essere spostato dopo l'assegnazione di p? Perché memory_order_seq presumo abbia ragione, voglio solo controllare. Buona risposta a BTW! – NoSenseEtAl

+0

BTW potresti approfondire perché 4) è la corsa dei dati? – NoSenseEtAl

+1

Sì, hai ragione in 5 --- i compiti su 'nonatomic_p [i]' non possono essere spostati dopo l'assegnazione di 'p'. –

7

Da quello che posso dire std :: memory_order_seq_cst inserisce barriera mem mentre altri valere solo ordinamento delle operazioni su certa posizione di memoria.

Dipende davvero da cosa stai facendo e su quale piattaforma stai lavorando. Il forte modello di ordinamento della memoria su una piattaforma come x86 creerà un diverso insieme di requisiti per l'esistenza di operazioni di memorizzazione della memoria rispetto a un modello di ordini debole su piattaforme come IA64, PowerPC, ARM, ecc. Ciò che il parametro predefinito di std::memory_order_seq_cst sta assicurando è che a seconda della piattaforma, verranno utilizzate le istruzioni di recinzione della memoria appropriate. Su una piattaforma come x86, non è necessaria una barriera di memoria completa a meno che non si stia eseguendo un'operazione di lettura-modifica-scrittura. Per il modello di memoria x86, tutti i carichi hanno una semantica di acquisizione del carico e tutti i negozi hanno una semantica di rilascio del negozio. Pertanto, in questi casi l'enum std::memory_order_seq_cst crea sostanzialmente un no-op poiché il modello di memoria per x86 garantisce già che quei tipi di operazioni siano coerenti tra i thread e quindi non ci sono istruzioni di assemblaggio che implementano questi tipi di barriere di memoria parziali. Pertanto, la stessa condizione no-op sarebbe vera se si imposta esplicitamente un'impostazione std::memory_order_release o std::memory_order_acquire su x86. Inoltre, richiedere una barriera di memoria completa in queste situazioni sarebbe un inutile ostacolo alle prestazioni. Come notato, sarebbe richiesto solo per le operazioni di modifica della lettura.

Su altre piattaforme con modelli di consistenza della memoria più deboli, tuttavia, non sarebbe il caso, e pertanto l'uso di std::memory_order_seq_cst impiegherebbe le corrette operazioni di memorizzazione della memoria senza che l'utente debba specificare esplicitamente se desidera un caricamento-acquisizione, rilascio, o operazione di recinzione di memoria completa. Queste piattaforme hanno istruzioni specifiche per l'applicazione di tali contratti di consistenza della memoria e l'impostazione std::memory_order_seq_cst risolverà il caso corretto. Se l'utente desidera chiamare specificamente una di queste operazioni, può farlo attraverso i tipi di enum espliciti std::memory_order, ma non sarebbe necessario ... il compilatore elaborerebbe le impostazioni corrette.

presumo un sacco di persone stanno andando essere fare errori orribili utilizzando std :: atomica, specialmente se essi non utilizzare di default (std :: memory_order_seq_cst memoria ordinazione)

Sì, se don' Sappiamo cosa stanno facendo e non capiamo quali tipi di semantica della barriera di memoria sono richieste in certe operazioni, quindi ci saranno molti errori commessi se tentano di dichiarare esplicitamente il tipo di barriera di memoria ed è il uno errato, specialmente su piattaforme che non aiuteranno la loro errata comprensione dell'ordine di memoria perché sono di natura più debole.

Infine, tenere a mente con la vostra situazione # 4, relativa ad un mutex che ci sono due cose diverse che devono accadere qui:

  1. Il compilatore non deve essere permesso di riordinare le operazioni in tutto il mutex e sezione critica (specialmente nel caso di un compilatore di ottimizzazione)
  2. Ci devono essere le necessarie schermate di memoria create (a seconda della piattaforma) che mantengono uno stato in cui tutti i negozi sono completati prima della sezione critica e della lettura della variabile mutex, e tutti i negozi sono completati prima di uscire dalla sezione critica.

Poiché per impostazione predefinita, negozi atomici e carichi vengono implementati con std::memory_order_seq_cst, quindi utilizzando atomiche sarebbe anche attuare i meccanismi idonei a soddisfare le condizioni # 1 e # 2. Detto questo, nel tuo primo esempio con l'atomica, il carico imporrebbe la semantica acquisita per il blocco, mentre il negozio imporrebbe la semantica del rilascio. Tuttavia, non imporrebbe alcun ordinamento particolare all'interno della "sezione critica" tra queste due operazioni. Nel tuo secondo esempio, hai due sezioni diverse con i blocchi, ognuno dei quali ha acquisito la semantica. Dato che a un certo punto dovresti rilasciare i blocchi, che avrebbero una semantica di rilascio, allora no, i due blocchi di codice non sarebbero equivalenti. Nel primo esempio, hai creato una grande "sezione critica" tra il carico e lo store (supponendo che tutto ciò avvenga sullo stesso thread). Nel secondo esempio hai due diverse sezioni critiche.

P.S. Ho trovato il seguente PDF particolarmente istruttivo, e si possono trovare anche: http://www.nwcpp.org/Downloads/2008/Memory_Fences.pdf

+0

Penso che (nel tuo n. 2) carichi e depositi prima che la sezione critica sia inserita possono essere spostati nella sezione critica E caricano e memorizzano dopo che il CS può essere spostato nel CS. Ciò significa che il compilatore può ancora riordinare (in una certa misura) i carichi e i depositi non correlati al CS attorno ai confini del CS. Questo perché quei carichi/negozi non erano originariamente parte del CS. Ma nulla può essere spostato da prima del CS a dopo il CS .. solo nel mezzo di esso. – SoapBox

+0

Sì, è corretto, almeno come capisco il significato di acquisire e rilasciare semantica. – Jason

+0

È possibile creare il proprio mutex funzionante fuori dalla suite di operazioni atomiche che viene fornita in C++ 11? – Omnifarious