2016-04-08 32 views
13

Sto eseguendo un thread che viene eseguito finché non viene impostato un flag.Dovrebbe essere std :: atomic volatile?

std::atomic<bool> stop(false); 

void f() { 
    while(!stop.load(std::memory_order_{relaxed,acquire})) { 
    do_the_job(); 
    } 
} 

Mi chiedo se il compilatore può srotolare il ciclo come questo (non voglio che accada).

void f() { 
    while(!stop.load(std::memory_order_{relaxed,acquire})) { 
    do_the_job(); 
    do_the_job(); 
    do_the_job(); 
    do_the_job(); 
    ... // unroll as many as the compiler wants 
    } 
} 

Si dice che la volatilità e atomicità sono ortogonali, ma io sono un po 'confuso. Il compilatore è libero di memorizzare nella cache il valore della variabile atomica e srotolare il ciclo? Se il compilatore può srotolare il ciclo, allora penso di dover mettere volatile alla bandiera, e voglio essere sicuro.

Devo inserire volatile?


Mi dispiace per essere ambiguo. Io (suppongo che io) capisco cosa sia il riordino e cosa significhi memory_order_* s, e sono sicuro di capire appieno cosa sia lo volatile.

Penso che il ciclo while() possa essere trasformato come un infinito if dichiarazioni come questa.

void f() { 
    if(stop.load(std::memory_order_{relaxed,acquire})) return; 
    do_the_job(); 
    if(stop.load(std::memory_order_{relaxed,acquire})) return; 
    do_the_job(); 
    if(stop.load(std::memory_order_{relaxed,acquire})) return; 
    do_the_job(); 
    ... 
} 

Dal momento che i dati degli ordini di memoria non impediscono le operazioni di sequenza-prima di essere spostato oltre il carico atomica, penso che possa essere ridefinita se è senza volatile.

void f() { 
    if(stop.load(std::memory_order_{relaxed,acquire})) return; 
    if(stop.load(std::memory_order_{relaxed,acquire})) return; 
    if(stop.load(std::memory_order_{relaxed,acquire})) return; 
    ... 
    do_the_job(); 
    do_the_job(); 
    do_the_job(); 
    ... 
} 

Se l'atomica non implica volatile, quindi penso che il codice può essere anche trasformato in questo modo al caso peggiore.

void f() { 
    if(stop.load(std::memory_order_{relaxed,acquire})) return; 

    while(true) { 
    do_the_job(); 
    } 
} 

Non ci sarà mai un'implementazione folle, ma immagino sia ancora una situazione possibile. Penso che l'unico modo per evitare questo è di mettere volatile alla variabile atomica e sto chiedendo su di esso.

Ci sono molte supposizioni che ho fatto, per favore dimmi se c'è qualcosa di sbagliato in loro.

+0

Io non la penso così. Ho guardato molto per 'std :: atomic' ultimamente, ma nessuno ha detto che dovrebbe essere. Immagino che all'interno della classe ci sia una variabile 'volatile' da qualche parte. – Nick

+2

Possibile duplicato di [Concorrenza: atomico e volatile nel modello di memoria C++ 11] (http://stackoverflow.com/questions/8819095/concurrency-atomic-and-volatile-in-c11-memory-model) –

+1

No, non dovrebbe essere volatile. –

risposta

7

Il compilatore è libero di memorizzare nella cache il valore della variabile atomica e srotolare il ciclo?

Il compilatore non può memorizzare nella cache il valore di una variabile atomica.

Tuttavia, dal momento che si utilizza std::memory_order_relaxed, ciò significa che il compilatore è libero di riordinare i carichi e gli archivi da/a questa variabile atomica rispetto ad altri carichi e negozi.

Si noti inoltre che una chiamata a una funzione la cui definizione non è disponibile in questa unità di traduzione è una barriera di memoria del compilatore. Ciò significa che la chiamata non può essere riordinata per quanto riguarda i carichi e i negozi circostanti e che tutte le variabili non locali devono essere ricaricate dalla memoria dopo la chiamata, come se fossero tutte contrassegnate come volatili. (Le variabili locali il cui indirizzo non è stato passato altrove non verranno comunque ricaricate).

Il trasformazione di codice che si desidera evitare, non sarebbe una trasformazione valida perché questo violerebbe modello di memoria C++: nel primo caso si ha un carico di una variabile atomica seguito da una chiamata a do_the_job, nel in secondo luogo, hai più chiamate.Il comportamento osservato del codice trasformato può essere diverso.


E una nota std::memory_order:

Rapporti con volatili

All'interno di un thread di esecuzione, accessi (letture e scritture) per tutti gli oggetti di volatili sono garantiti per non essere riordinati l'uno rispetto all'altro, ma questo ordine non è garantito per essere osservato da un altro thread, poiché l'accesso volatile non stabilisce la sincronizzazione tra thread.

Inoltre, accessi volatili non sono atomiche (simultaneo in lettura e scrittura è una corsa di dati) e non ordinare memoria (memoria non volatile accessi possono essere liberamente riordinate in tutto l'accesso volatili).

Questo bit memoria non volatile accede può essere liberamente riordinate poter accedere volatili vale per atomics rilassato così, poiché il carico e memorizza rilassata possono essere riordinate per quanto riguarda altri carichi e negozi.

In altre parole, decorare il tuo atomico con volatile non modifica il comportamento del codice.


Indipendentemente da ciò, C++ 11 variabili atomiche non hanno bisogno di essere contrassegnati con volatile parola chiave.


Ecco un esempio di come g ++ - 5.2 onori le variabili atomiche. Le seguenti funzioni:

__attribute__((noinline)) int f(std::atomic<int>& a) { 
    return a.load(std::memory_order_relaxed); 
} 

__attribute__((noinline)) int g(std::atomic<int>& a) { 
    static_cast<void>(a.load(std::memory_order_relaxed)); 
    static_cast<void>(a.load(std::memory_order_relaxed)); 
    static_cast<void>(a.load(std::memory_order_relaxed)); 
    return a.load(std::memory_order_relaxed); 
} 

__attribute__((noinline)) int h(std::atomic<int>& a) { 
    while(a.load(std::memory_order_relaxed)) 
     ; 
    return 0; 
} 

compilati con g++ -o- -Wall -Wextra -S -march=native -O3 -pthread -std=gnu++11 test.cc | c++filt > test.S producono il seguente montaggio:

f(std::atomic<int>&): 
    movl (%rdi), %eax 
    ret 

g(std::atomic<int>&): 
    movl (%rdi), %eax 
    movl (%rdi), %eax 
    movl (%rdi), %eax 
    movl (%rdi), %eax 
    ret 

h(std::atomic<int>&): 
.L4: 
    movl (%rdi), %eax 
    testl %eax, %eax 
    jne .L4 
    ret 
+0

Non penso che possiamo supporre che una funzione sarà una barriera del compilatore poiché c'è una bestia chiamata LTO. Intendi dire che anche due successive operazioni di carico atomico sulla stessa variabile non possono essere trasformate in un singolo carico? – kukyakya

+0

@kukyakya Il modello di memoria suggerisce che una variabile atomica può essere modificata da un altro thread, quindi il carico non può essere eliminato. Elidere il carico di una variabile atomica renderebbe i negozi alla variabile atomica invisibili ad altri thread, il che violerebbe il modello di memoria che garantisce la visibilità dei negozi alle variabili atomiche. –

+0

"che garantisce la visibilità dei negozi alle variabili atomiche" Non è garantito che un negozio diventi visibile ad altri carichi entro un periodo di tempo limitato; il meglio che abbiamo è "Le implementazioni dovrebbero rendere i depositi atomici visibili ai carichi atomici entro un ragionevole lasso di tempo", che è un incoraggiamento normativo ("dovrebbe"), non un requisito. –

2

Se do_the_job() non cambia stop, non importa se il compilatore può srotolare il ciclo, oppure no.

std::memory_order_relaxed assicura solo che ogni operazione sia atomica, ma non impedisce il riordino degli accessi. Ciò significa che se un altro thread imposta stop su true, il ciclo potrebbe continuare a essere eseguito alcune volte, poiché gli accessi potrebbero essere riordinati. Quindi è la stessa situazione di un loop srotolato: do_the_job() potrebbe essere eseguito alcune volte dopo che un altro thread ha impostato stop a true.

Quindi no, non utilizzare volatile, utilizzare std::memory_order_acquire e std::memory_order_release.

+0

Ho capito il tuo punto. Poiché non è garantito che l'operazione di caricamento ottenga l'ultimo valore, non ha senso limitare il numero di chiamate alla funzione. E il caso che ho aggiunto? – kukyakya

+0

Trovo difficile ragionare su questo senza sapere cosa fa do_the_job(), e cosa fa il thread che imposta 'stop'. C'è sicuramente più sincronizzazione tra i due se accedono ai dati comuni, penso. Puoi pubblicare un esempio più dettagliato? – alain