Sto cercando di rispondere da solo, dopo aver esaminato varie risorse online (ad esempio, questa e questa ), lo standard C ++ 11, nonché le risposte fornite qui.
Le domande correlate vengono unite (ad esempio, " perché! Atteso? " Viene unito a "perché mettere compare_exchange_weak () in un ciclo? ") E le risposte vengono fornite di conseguenza.
Perché compare_exchange_weak () deve essere in un ciclo in quasi tutti gli usi?
Pattern tipico A
È necessario ottenere un aggiornamento atomico basato sul valore nella variabile atomica. Un errore indica che la variabile non è stata aggiornata con il valore desiderato e si desidera riprovare. Nota che non ci interessa davvero se fallisce a causa di scritture simultanee o errori spuri. Ma ci interessa che siamo noi a fare questo cambiamento.
expected = current.load();
do desired = function(expected);
while (!current.compare_exchange_weak(expected, desired));
Un esempio del mondo reale è che diversi thread aggiungono contemporaneamente un elemento a un elenco collegato singolarmente. Ogni thread carica prima il puntatore della testina, alloca un nuovo nodo e aggiunge la testina a questo nuovo nodo. Infine, prova a scambiare il nuovo nodo con la testa.
Un altro esempio è implementare mutex usando std::atomic<bool>
. Al massimo un thread può entrare nella sezione critica alla volta, a seconda di quale thread è stato impostato current
per primo true
ed uscire dal ciclo.
Pattern tipico B
Questo è in realtà lo schema menzionato nel libro di Anthony. Contrariamente al modello A, vuoi che la variabile atomica venga aggiornata una volta, ma non ti interessa chi lo fa. Finché non è aggiornato, riprova. Viene generalmente utilizzato con le variabili booleane. Ad esempio, è necessario implementare un trigger per far andare avanti una macchina a stati. Quale filo tira il grilletto è indipendentemente.
expected = false;
while (!current.compare_exchange_weak(expected, true) && !expected);
Nota che generalmente non possiamo usare questo modello per implementare un mutex. In caso contrario, più thread potrebbero trovarsi all'interno della sezione critica allo stesso tempo.
Detto questo, dovrebbe essere raro da usare compare_exchange_weak()
al di fuori di un ciclo. Al contrario, ci sono casi in cui è in uso la versione forte. Per esempio,
bool criticalSection_tryEnter(lock)
{
bool flag = false;
return lock.compare_exchange_strong(flag, true);
}
compare_exchange_weak
non è corretto qui perché quando ritorna a causa di un guasto spurio, è probabile che nessuno occupi ancora la sezione critica.
Filo di fame?
Un punto degno di nota è che cosa succede se continuano a verificarsi errori spuri, facendo così morire di fame il filo? Teoricamente potrebbe accadere sulle piattaforme quando compare_exchange_XXX()
è implementato come una sequenza di istruzioni (ad esempio, LL / SC). L'accesso frequente alla stessa linea di cache tra LL e SC produrrà continui errori spuri. Un esempio più realistico è dovuto a una pianificazione stupida in cui tutti i thread simultanei vengono intercalati nel modo seguente.
Time
| thread 1 (LL)
| thread 2 (LL)
| thread 1 (compare, SC), fails spuriously due to thread 2's LL
| thread 1 (LL)
| thread 2 (compare, SC), fails spuriously due to thread 1's LL
| thread 2 (LL)
v ..
Può succedere?
Non succederà per sempre, fortunatamente, grazie a ciò che richiede C ++ 11:
Le implementazioni dovrebbero garantire che le operazioni di confronto e scambio deboli non restituiscano costantemente false a meno che l'oggetto atomico non abbia un valore diverso da quello previsto o non vi siano modifiche simultanee all'oggetto atomico.
Perché ci preoccupiamo di usare compare_exchange_weak () e scrivere il ciclo noi stessi? Possiamo semplicemente usare compare_exchange_strong ().
Dipende.
Caso 1: quando entrambi devono essere utilizzati all'interno di un loop. C ++ 11 dice:
Quando un confronto e scambio è in un ciclo, la versione debole produrrà prestazioni migliori su alcune piattaforme.
Su x86 (almeno attualmente. Forse un giorno ricorrerà a uno schema simile come LL / SC per le prestazioni quando vengono introdotti più core), la versione debole e forte sono essenzialmente le stesse perché entrambe si riducono alla singola istruzione cmpxchg
. Su alcune altre piattaforme in cui compare_exchange_XXX()
non è implementato atomicamente (qui significa che non esiste una singola primitiva hardware), la versione debole all'interno del ciclo potrebbe vincere la battaglia perché quella forte dovrà gestire i guasti spuri e riprovare di conseguenza.
Ma,
raramente, si può preferire compare_exchange_strong()
sopra compare_exchange_weak()
, anche in un ciclo. Ad esempio, quando ci sono molte cose da fare tra la variabile atomica viene caricata e un nuovo valore calcolato viene scambiato (vedi function()
sopra). Se la variabile atomica stessa non cambia frequentemente, non è necessario ripetere il costoso calcolo per ogni errore spurio. Invece, possiamo sperare che compare_exchange_strong()
"assorbano" tali errori e ripetiamo il calcolo solo quando fallisce a causa di un cambiamento di valore reale.
Caso 2: quando compare_exchange_weak()
deve essere utilizzato solo all'interno di un loop. C ++ 11 dice anche:
Quando un confronto e scambio debole richiederebbe un ciclo e uno forte no, quello forte è preferibile.
Questo è in genere il caso in cui si esegue il ciclo solo per eliminare errori spuri dalla versione debole. Riprovare fino a quando lo scambio non ha esito positivo o negativo a causa della scrittura simultanea.
expected = false;
while (!current.compare_exchange_weak(expected, true) && !expected);
Nella migliore delle ipotesi, sta reinventando le ruote e ha le stesse prestazioni di compare_exchange_strong()
. Peggio? Questo approccio non riesce a sfruttare appieno le macchine che forniscono un confronto e uno scambio non spuri nell'hardware .
Infine, se si esegue un ciclo per altre cose (ad esempio, vedere "Modello tipico A" sopra), allora ci sono buone possibilità che compare_exchange_strong()
venga inserito in un ciclo, il che ci riporta al caso precedente.