Cosa è garantito con C ++ std :: atomic a livello di programmatore?


9

Ho ascoltato e letto diversi articoli, discorsi e domande su StackOverflow std::atomice vorrei essere sicuro di averlo capito bene. Perché sono ancora un po 'confuso con la riga della cache che scrive la visibilità a causa di possibili ritardi nei protocolli di coerenza della cache MESI (o derivati), buffer di archivio, code non valide e così via.

Ho letto che x86 ha un modello di memoria più forte e che se una invalidazione della cache viene ritardata, x86 può ripristinare le operazioni avviate. Ma ora sono interessato solo a ciò che dovrei assumere come programmatore C ++, indipendentemente dalla piattaforma.

[T1: thread1 T2: thread2 V1: variabile atomica condivisa]

Capisco che std :: atomic garantisce che,

(1) Non si verificano corse di dati su una variabile (grazie all'accesso esclusivo alla riga della cache).

(2) A seconda di quale memory_order utilizziamo, garantisce (con barriere) che si verifichi la coerenza sequenziale (prima di una barriera, dopo una barriera o entrambe).

(3) Dopo una scrittura atomica (V1) su T1, una RMW atomica (V1) su T2 sarà coerente (la sua riga della cache sarà stata aggiornata con il valore scritto su T1).

Ma come menziona il primer di coerenza della cache ,

L'implicazione di tutte queste cose è che, per impostazione predefinita, i carichi possono recuperare dati non aggiornati (se una corrispondente richiesta di invalidazione si trovava nella coda di invalidazione)

Quindi, è il seguente corretto?

(4) std::atomicNON garantisce che T2 non legga un valore "stantio" su una lettura atomica (V) dopo una scrittura atomica (V) su T1.

Domande se (4) è giusto: se la scrittura atomica su T1 invalida la riga della cache indipendentemente dal ritardo, perché T2 sta aspettando che l'invalidazione sia efficace quando viene eseguita un'operazione RMW atomica ma non su una lettura atomica?

Domande se (4) è sbagliato: quando un thread può leggere un valore "stantio" e "è visibile" nell'esecuzione, allora?

Apprezzo molto le tue risposte

Aggiornamento 1

Quindi sembra che mi sia sbagliato su (3) allora. Immagina il seguente interleave, per un iniziale V1 = 0:

T1: W(1)
T2:      R(0) M(++) W(1)

Anche se in questo caso l'RMW di T2 si verifica interamente dopo W (1), può comunque leggere un valore "stantio" (ho sbagliato). In base a ciò, Atomic non garantisce la piena coerenza della cache, ma solo la coerenza sequenziale.

Aggiornamento 2

(5) Ora immagina questo esempio (x = y = 0 e sono atomici):

T1: x = 1;
T2: y = 1;
T3: if (x==1 && y==0) print("msg");

in base a ciò di cui abbiamo parlato, vedere "msg" visualizzato sullo schermo non ci fornirebbe informazioni oltre al fatto che T2 è stato eseguito dopo T1. Quindi potrebbe essere avvenuta una delle seguenti esecuzioni:

  • T1 <T3 <T2
  • T1 <T2 <T3 (dove T3 vede x = 1 ma non y = 1 ancora)

è giusto?

(6) Se un thread è sempre in grado di leggere valori "non aggiornati", cosa accadrebbe se prendessimo il tipico scenario di "pubblicazione" ma invece di segnalare che alcuni dati sono pronti, facciamo esattamente il contrario (eliminiamo i dati)?

T1: delete gameObjectPtr; is_enabled.store(false, std::memory_order_release);
T2: while (is_enabled.load(std::memory_order_acquire)) gameObjectPtr->doSomething();

dove T2 continuerebbe a usare un ptr eliminato fino a quando is_enabled è falso.

(7) Inoltre, il fatto che i thread possano leggere valori "non aggiornati" significa che un mutex non può essere implementato con un solo atomico senza lock, giusto? Richiederebbe un meccanismo di sincronizzazione tra i thread. Richiederebbe un atomico bloccabile?

Risposte:


3
  1. Sì, non ci sono gare di dati
  2. Sì, con memory_ordervalori appropriati è possibile garantire la coerenza sequenziale
  3. Una lettura / modifica-scrittura atomica avverrà sempre interamente prima o dopo una scrittura atomica sulla stessa variabile
  4. Sì, T2 può leggere un valore non aggiornato da una variabile dopo una scrittura atomica su T1

Le operazioni atomiche di lettura-modifica-scrittura sono specificate in modo da garantire la loro atomicità. Se un altro thread potesse scrivere sul valore dopo la lettura iniziale e prima della scrittura di un'operazione RMW, tale operazione non sarebbe atomica.

I thread possono sempre leggere valori non aggiornati, tranne quando accade prima che garantisca un ordine relativo .

Se un'operazione RMW legge un valore "non aggiornato", allora garantisce che la scrittura che genera sarà visibile prima di eventuali scritture da altri thread che sovrascriverebbero il valore letto.

Aggiornamento per esempio

Se T1 scrive x=1e T2 fa x++, con xinizialmente 0, le scelte dal punto di vista della memorizzazione di xsono:

  1. La scrittura di T1 è la prima, quindi T1 scrive x=1, quindi T2 legge x==1, incrementa quella a 2 e riscrive x=2come una singola operazione atomica.

  2. La scrittura di T1 è seconda. T2 legge x==0, lo incrementa a 1 e riscrive x=1come una singola operazione, quindi T1 scrive x=1.

Tuttavia, purché non vi siano altri punti di sincronizzazione tra questi due thread, i thread possono procedere con le operazioni non scaricate in memoria.

Quindi T1 può emettere x=1, quindi procedere con altre cose, anche se T2 continuerà a leggere x==0(e quindi a scrivere x=1).

Se ci sono altri punti di sincronizzazione, diventerà evidente quale thread è stato modificato per xprimo, poiché quei punti di sincronizzazione forzeranno un ordine.

Ciò è più evidente se si ha una condizione sul valore letto da un'operazione RMW.

Aggiornamento 2

  1. Se usi memory_order_seq_cst(impostazione predefinita) per tutte le operazioni atomiche non devi preoccuparti di questo genere di cose. Dal punto di vista del programma, se vedi "msg", poi T1 ha funzionato, quindi T3, quindi T2.

Se usi altri ordini di memoria (in particolare memory_order_relaxed), potresti vedere altri scenari nel tuo codice.

  1. In questo caso, hai un bug. Supponiamo che la is_enabledbandiera sia vera, quando T2 entra nel suo whileciclo, quindi decide di eseguire il corpo. T1 ora elimina i dati e T2 quindi differisce il puntatore, che è un puntatore penzolante, e ne consegue un comportamento indefinito . L'atomica non aiuta o ostacola in alcun modo oltre a prevenire la corsa dei dati sulla bandiera.

  2. È possibile implementare un mutex con una singola variabile atomica.


Grazie mille @Anthony Wiliams per la tua rapida risposta. Ho aggiornato la mia domanda con un esempio di RMW che legge un valore "non aggiornato". Guardando questo esempio, cosa intendi per ordine relativo e che T2 W (1) sarà visibile prima di ogni scrittura? Significa che una volta che T2 ha visto i cambiamenti di T1, non leggerà più W (1) di T2?
Albert Caldas,

Quindi, se "I thread possono sempre leggere valori non aggiornati" significa che la coerenza della cache non è mai garantita (almeno a livello di programmatore c ++). Potresti dare un'occhiata al mio aggiornamento2 per favore?
Albert Caldas,

Ora vedo che avrei dovuto prestare maggiore attenzione al linguaggio e ai modelli di memoria hardware per comprendere appieno tutto ciò, quello era il pezzo che mi mancava. molte grazie!
Albert Caldas,

1

Per quanto riguarda (3) - dipende dall'ordine di memoria utilizzato. Se entrambi, il negozio e l'operazione RMW utilizzano std::memory_order_seq_cst, entrambe le operazioni vengono ordinate in qualche modo, ovvero il negozio avviene prima dell'RMW o viceversa. Se l'archivio viene ordinato prima dell'RMW, allora è garantito che l'operazione RMW "vede" il valore che è stato archiviato. Se il negozio viene ordinato dopo l'RMW, sovrascriverà il valore scritto dall'operazione RMW.

Se usi ordini di memoria più rilassati, le modifiche verranno comunque ordinate in qualche modo (l'ordine di modifica della variabile), ma non hai garanzie sul fatto che RMW "veda" il valore dall'operazione di memorizzazione, anche se l'operazione RMW è ordine dopo la scrittura nell'ordine di modifica della variabile.

Nel caso in cui desideri leggere ancora un altro articolo, posso fare riferimento a Modelli di memoria per programmatori C / C ++ .


Grazie per l'articolo, non l'avevo ancora letto. Anche se è piuttosto vecchio, è stato utile mettere insieme le mie idee.
Albert Caldas,

1
Sono contento di sentirlo: questo articolo è un capitolo leggermente ampliato e rivisto dalla tesi del mio maestro. :-) Si concentra sul modello di memoria come introdotto C ++ 11; Potrei aggiornarlo per riflettere le (piccole) modifiche introdotte in C ++ 14/17. Per favore fatemi sapere se avete commenti o suggerimenti per miglioramenti!
mpoeter
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.