C ++ 11 ha introdotto un modello di memoria standardizzato. Cosa significa? E come influenzerà la programmazione C ++?


1894

C ++ 11 ha introdotto un modello di memoria standardizzato, ma cosa significa esattamente? E come influenzerà la programmazione C ++?

Questo articolo (di Gavin Clarke che cita Herb Sutter ) afferma che,

Il modello di memoria significa che il codice C ++ ora ha una libreria standardizzata da chiamare indipendentemente da chi ha creato il compilatore e su quale piattaforma è in esecuzione. Esiste un modo standard per controllare il modo in cui thread diversi parlano alla memoria del processore.

"Quando parli di dividere [codice] su diversi core che sono nello standard, stiamo parlando del modello di memoria. Lo ottimizzeremo senza infrangere i seguenti presupposti che le persone faranno nel codice", ha detto Sutter .

Bene, posso memorizzare questo e altri paragrafi simili disponibili online (dato che ho avuto il mio modello di memoria sin dalla nascita: P) e posso anche pubblicare come risposta a domande poste da altri, ma ad essere sincero, non capisco esattamente Questo.

I programmatori C ++ erano soliti sviluppare applicazioni multi-thread anche prima, quindi che importanza ha se si tratta di thread POSIX, thread di Windows o thread C ++ 11? Quali sono i vantaggi? Voglio capire i dettagli di basso livello.

Ho anche la sensazione che il modello di memoria C ++ 11 sia in qualche modo correlato al supporto multi-threading C ++ 11, come spesso vedo questi due insieme. Se lo è, come esattamente? Perché dovrebbero essere correlati?

Dato che non so come funzionano gli interni del multi-threading e cosa significhi il modello di memoria in generale, aiutami a capire questi concetti. :-)


3
@curiousguy: Elaborate ...
Nawaz,

4
@curiousguy: scrivi un blog quindi ... e proponi anche una soluzione. Non c'è altro modo per rendere valido e logico il tuo punto.
Nawaz,

2
Ho scambiato quel sito come un posto per chiedere Q e scambiare idee. Colpa mia; è il luogo della conformità dove non puoi essere in disaccordo con Herb Sutter anche quando si contraddice in modo flagrante riguardo alle specifiche di lancio.
curiousguy,

5
@curiousguy: C ++ è ciò che dice lo Standard, non quello che dice un ragazzo a caso su Internet. Quindi sì, ci deve essere conformità allo standard. Il C ++ NON è una filosofia aperta in cui puoi parlare di tutto ciò che non è conforme allo Standard.
Nawaz,

3
"Ho dimostrato che nessun programma C ++ può avere un comportamento ben definito." . Dichiarazioni elevate, senza alcuna prova!
Nawaz,

Risposte:


2205

Innanzitutto, devi imparare a pensare come un avvocato linguistico.

La specifica C ++ non fa riferimento a nessun compilatore, sistema operativo o CPU particolare. Fa riferimento a una macchina astratta che è una generalizzazione di sistemi reali. Nel mondo di Language Lawyer, il compito del programmatore è scrivere codice per la macchina astratta; il compito del compilatore è attualizzare quel codice su una macchina concreta. Codificando rigidamente le specifiche, si può essere certi che il codice verrà compilato ed eseguito senza modifiche su qualsiasi sistema con un compilatore C ++ conforme, sia oggi che tra 50 anni.

La macchina astratta nella specifica C ++ 98 / C ++ 03 è fondamentalmente a thread singolo. Quindi non è possibile scrivere codice C ++ multi-thread "completamente portabile" rispetto alle specifiche. Le specifiche non dicono nemmeno nulla sull'atomicità dei carichi e degli archivi di memoria o sull'ordine in cui potrebbero accadere carichi e archivi, non importa cose come i mutex.

Ovviamente, puoi scrivere codice multi-thread in pratica per particolari sistemi concreti, come pthreads o Windows. Ma non esiste un modo standard per scrivere codice multi-thread per C ++ 98 / C ++ 03.

La macchina astratta in C ++ 11 è multi-thread dal design. Ha anche un modello di memoria ben definito ; vale a dire ciò che il compilatore può e non può fare quando si tratta di accedere alla memoria.

Considera l'esempio seguente, in cui due coppie di thread accedono contemporaneamente a due thread:

           Global
           int x, y;

Thread 1            Thread 2
x = 17;             cout << y << " ";
y = 37;             cout << x << endl;

Cosa potrebbe essere l'output Thread 2?

In C ++ 98 / C ++ 03, questo non è nemmeno un comportamento indefinito; la domanda stessa non ha senso perché lo standard non contempla nulla chiamato "thread".

In C ++ 11, il risultato è Undefined Behaviour, poiché i carichi e gli archivi non devono essere atomici in generale. Che potrebbe non sembrare un grande miglioramento ... E da solo, non lo è.

Ma con C ++ 11, puoi scrivere questo:

           Global
           atomic<int> x, y;

Thread 1                 Thread 2
x.store(17);             cout << y.load() << " ";
y.store(37);             cout << x.load() << endl;

Ora le cose diventano molto più interessanti. Prima di tutto, il comportamento qui è definito . Il thread 2 ora può stampare 0 0(se viene eseguito prima del thread 1), 37 17(se viene eseguito dopo il thread 1) o 0 17(se viene eseguito dopo il thread 1 viene assegnato a x ma prima che venga assegnato a y).

Ciò che non è possibile stampare è 37 0perché la modalità predefinita per i carichi / negozi atomici in C ++ 11 è quella di imporre la coerenza sequenziale . Questo significa solo che tutti i carichi e i negozi devono essere "come se" avvenissero nell'ordine in cui li hai scritti all'interno di ogni thread, mentre le operazioni tra i thread possono essere intercalate a piacimento del sistema. Quindi il comportamento predefinito dell'atomica fornisce sia atomicità che ordini per carichi e depositi.

Ora, su una CPU moderna, garantire la coerenza sequenziale può essere costoso. In particolare, è probabile che il compilatore emetta barriere di memoria complete tra tutti gli accessi qui. Ma se il tuo algoritmo può tollerare carichi e depositi fuori ordine; cioè, se richiede atomicità ma non ordinamento; cioè, se può tollerare 37 0come output da questo programma, allora puoi scrivere questo:

           Global
           atomic<int> x, y;

Thread 1                            Thread 2
x.store(17,memory_order_relaxed);   cout << y.load(memory_order_relaxed) << " ";
y.store(37,memory_order_relaxed);   cout << x.load(memory_order_relaxed) << endl;

Più moderna è la CPU, più è probabile che sia più veloce dell'esempio precedente.

Infine, se hai solo bisogno di mantenere carichi e magazzini particolari in ordine, puoi scrivere:

           Global
           atomic<int> x, y;

Thread 1                            Thread 2
x.store(17,memory_order_release);   cout << y.load(memory_order_acquire) << " ";
y.store(37,memory_order_release);   cout << x.load(memory_order_acquire) << endl;

Questo ci riporta ai carichi e ai depositi ordinati - quindi 37 0non è più un output possibile - ma lo fa con un sovraccarico minimo. (In questo banale esempio, il risultato è lo stesso della consistenza sequenziale in piena regola; in un programma più ampio, non lo sarebbe.)

Naturalmente, se gli unici output che vuoi vedere sono 0 0o 37 17, puoi semplicemente avvolgere un mutex attorno al codice originale. Ma se hai letto fino a questo punto, scommetto che sai già come funziona, e questa risposta è già più lunga di quanto volessi :-).

Quindi, linea di fondo. I mutex sono fantastici e C ++ 11 li standardizza. Ma a volte per motivi di prestazioni si desidera primitivi di livello inferiore (ad esempio, il classico modello di blocco con doppio controllo ). Il nuovo standard fornisce gadget di alto livello come mutex e variabili di condizione e fornisce anche gadget di basso livello come tipi atomici e vari tipi di barriera di memoria. Quindi ora puoi scrivere routine simultanee sofisticate e ad alte prestazioni interamente nella lingua specificata dallo standard e puoi essere certo che il tuo codice verrà compilato ed eseguito invariato sia sui sistemi di oggi che su quelli di domani.

Sebbene sia sincero, a meno che tu non sia un esperto e lavori su un codice di basso livello, probabilmente dovresti attenerti ai mutex e alle variabili delle condizioni. Questo è quello che intendo fare.

Per ulteriori informazioni su queste cose, vedere questo post del blog .


37
Bella risposta, ma questo sta davvero implorando alcuni esempi concreti dei nuovi primitivi. Inoltre, penso che l'ordinamento della memoria senza primitive sia lo stesso di pre-C ++ 0x: non ci sono garanzie.
John Ripley,

5
@Giovanni: lo so, ma sto ancora imparando da solo i primitivi :-). Inoltre penso che garantiscano che gli accessi ai byte siano atomici (anche se non ordinati), per questo sono andato con "char" per il mio esempio ... Ma non ne sono nemmeno sicuro al 100% ... Se vuoi suggerire qualcosa di buono " tutorial "riferimenti li aggiungerò alla mia risposta
Nemo

48
@Nawaz: Sì! Gli accessi alla memoria possono essere riordinati dal compilatore o dalla CPU. Pensa (ad esempio) alle cache e ai carichi speculativi. L'ordine in cui viene colpita la memoria di sistema non può essere simile a quello che hai codificato. Il compilatore e la CPU assicureranno che tali riordini non rompano il codice a thread singolo . Per il codice multi-thread, il "modello di memoria" caratterizza i possibili riordini e cosa succede se due thread leggono / scrivono la stessa posizione contemporaneamente e come si esercita il controllo su entrambi. Per il codice a thread singolo, il modello di memoria è irrilevante.
Nemo,

26
@Nawaz, @Nemo - Un dettaglio minore: il nuovo modello di memoria è rilevante nel codice a thread singolo nella misura in cui specifica la non definizione di alcune espressioni, come ad esempio i = i++. Il vecchio concetto di punti sequenza è stato scartato; il nuovo standard specifica la stessa cosa usando una relazione sequenziata prima che è solo un caso speciale del concetto più generale di inter-thread che precede .
JohannesD,

17
@ AJG85: la sezione 3.6.2 della bozza delle specifiche C ++ 0x dice "Le variabili con durata di memorizzazione statica (3.7.1) o durata di memorizzazione del thread (3.7.2) devono essere inizializzate a zero (8.5) prima che qualsiasi altra inizializzazione richieda posto." Dal momento che x, y sono globali in questo esempio, hanno una durata di archiviazione statica e quindi verranno inizializzati a zero, credo.
Nemo,

345

Darò solo l'analogia con cui comprendo i modelli di coerenza della memoria (o modelli di memoria, in breve). È ispirato al documento fondamentale di Leslie Lamport "Time, Clocks and the Ordering of Events in a Distributed System" . L'analogia è appropriata e ha un significato fondamentale, ma può essere eccessiva per molte persone. Tuttavia, spero che fornisca un'immagine mentale (una rappresentazione pittorica) che faciliti il ​​ragionamento sui modelli di coerenza della memoria.

Vediamo le storie di tutte le posizioni di memoria in un diagramma spazio-temporale in cui l'asse orizzontale rappresenta lo spazio degli indirizzi (ovvero, ogni posizione di memoria è rappresentata da un punto su quell'asse) e l'asse verticale rappresenta il tempo (vedremo che, in generale, non esiste una nozione universale di tempo). La cronologia dei valori detenuti da ciascuna posizione di memoria è, quindi, rappresentata da una colonna verticale a quell'indirizzo di memoria. Ogni modifica del valore è dovuta a uno dei thread che scrive un nuovo valore in quella posizione. Per immagine di memoria , intendiamo l'aggregazione / combinazione di valori di tutte le posizioni di memoria osservabili in un determinato momento da un particolare thread .

Citando da "Un primer sulla coerenza della memoria e la coerenza della cache"

Il modello di memoria intuitivo (e più restrittivo) è la coerenza sequenziale (SC) in cui un'esecuzione multithread dovrebbe apparire come un interfogliamento delle esecuzioni sequenziali di ciascun thread costituente, come se i thread fossero multiplexati nel tempo su un processore single-core.

L'ordine di memoria globale può variare da un'esecuzione del programma a un'altra e potrebbe non essere noto in anticipo. La caratteristica di SC è l'insieme di sezioni orizzontali nel diagramma indirizzo-spazio-tempo che rappresentano i piani di simultaneità (cioè le immagini di memoria). Su un dato piano, tutti i suoi eventi (o valori di memoria) sono simultanei. C'è una nozione di Absolute Time , in cui tutti i thread concordano su quali valori di memoria sono simultanei. In SC, in ogni istante, esiste una sola immagine di memoria condivisa da tutti i thread. Questo è, in ogni istante di tempo, tutti i processori concordano sull'immagine di memoria (cioè il contenuto aggregato della memoria). Ciò implica non solo che tutti i thread visualizzano la stessa sequenza di valori per tutte le posizioni di memoria, ma anche che tutti i processori osservano la stessacombinazioni di valori di tutte le variabili. Ciò equivale a dire che tutte le operazioni di memoria (su tutte le posizioni di memoria) sono osservate nello stesso ordine totale da tutti i thread.

Nei modelli di memoria rilassati, ogni thread suddividerà gli indirizzi-spazio-tempo a modo suo, l'unica limitazione è che le sezioni di ciascun thread non si incrociano perché tutti i thread devono concordare sulla storia di ogni singola posizione di memoria (ovviamente , sezioni di fili diversi possono e si incrociano). Non esiste un modo universale per dividerlo (nessuna foliazione privilegiata di indirizzo-spazio-tempo). Le fette non devono essere planari (o lineari). Possono essere curvi e questo è ciò che può fare in modo che un thread legga i valori scritti da un altro thread nell'ordine in cui sono stati scritti. Storie di posizioni di memoria diverse possono scorrere (o allungarsi) arbitrariamente l'una rispetto all'altra quando visualizzate da un particolare thread. Ogni thread avrà un diverso senso di quali eventi (o, equivalentemente, valori di memoria) sono simultanei. L'insieme di eventi (o valori di memoria) che sono simultanei a un thread non sono simultanei a un altro. Pertanto, in un modello di memoria rilassato, tutti i thread osservano ancora la stessa cronologia (ovvero la sequenza di valori) per ogni posizione di memoria. Ma possono osservare diverse immagini di memoria (cioè combinazioni di valori di tutte le posizioni di memoria). Anche se due diverse posizioni di memoria sono scritte dallo stesso thread in sequenza, i due valori appena scritti possono essere osservati in ordine diverso da altri thread.

[Immagine da Wikipedia] Foto da Wikipedia

I lettori che hanno familiarità con la teoria della relatività speciale di Einstein noteranno ciò a cui alludo. Tradurre le parole di Minkowski nel regno dei modelli di memoria: lo spazio degli indirizzi e il tempo sono ombre dello spazio-spazio-tempo. In questo caso, ciascun osservatore (ad es. Thread) proietterà ombre di eventi (ad es. Memorie / carichi di memoria) sulla propria linea del mondo (ovvero il suo asse del tempo) e il proprio piano di simultaneità (il suo asse dello spazio degli indirizzi) . I thread nel modello di memoria C ++ 11 corrispondono agli osservatori che si muovono l'uno rispetto all'altro nella relatività speciale. La coerenza sequenziale corrisponde allo spazio-tempo galileo (cioè, tutti gli osservatori concordano su un ordine assoluto di eventi e un senso globale di simultaneità).

La somiglianza tra modelli di memoria e relatività speciale deriva dal fatto che entrambi definiscono un insieme di eventi parzialmente ordinato, spesso chiamato insieme causale. Alcuni eventi (ad es. Archivi di memoria) possono influenzare (ma non essere influenzati da) altri eventi. Un thread C ++ 11 (o un osservatore in fisica) non è altro che una catena (cioè un insieme totalmente ordinato) di eventi (ad esempio, la memoria carica e memorizza in indirizzi eventualmente diversi).

Nella relatività, un certo ordine viene riportato al quadro apparentemente caotico di eventi parzialmente ordinati, poiché l'unico ordinamento temporale su cui tutti gli osservatori concordano è l'ordinamento tra eventi “simili al tempo” (cioè quegli eventi che sono in linea di principio collegabili a qualsiasi particella che va più lentamente rispetto alla velocità della luce nel vuoto). Solo gli eventi relativi al tempo sono ordinati in modo invariante. Time in Physics, Craig Callender .

Nel modello di memoria C ++ 11, un meccanismo simile (il modello di coerenza acquisizione-rilascio) viene utilizzato per stabilire queste relazioni di causalità locali .

Per fornire una definizione di coerenza della memoria e una motivazione per abbandonare SC, citerò "A Primer on Coerence and Memory Cherence"

Per una macchina a memoria condivisa, il modello di coerenza della memoria definisce il comportamento architettonicamente visibile del suo sistema di memoria. Il criterio di correttezza per un singolo core del processore divide il comportamento tra " un risultato corretto " e " molte alternative errate ". Questo perché l'architettura del processore impone che l'esecuzione di un thread trasformi un determinato stato di input in un singolo stato di output ben definito, anche su un core fuori servizio. I modelli di coerenza della memoria condivisa, tuttavia, riguardano i carichi e gli archivi di più thread e in genere consentono molte esecuzioni correttevietando molti (più) errati. La possibilità di eseguire più esecuzioni corrette è dovuta all'ISA che consente l'esecuzione simultanea di più thread, spesso con molti possibili intrecci legali di istruzioni da thread diversi.

I modelli di coerenza della memoria rilassati o deboli sono motivati ​​dal fatto che la maggior parte degli ordini di memoria nei modelli forti non sono necessari. Se un thread aggiorna dieci elementi di dati e quindi un flag di sincronizzazione, ai programmatori di solito non importa se gli elementi di dati vengono aggiornati l'uno rispetto all'altro, ma solo che tutti gli elementi di dati vengono aggiornati prima dell'aggiornamento del flag (di solito implementato usando le istruzioni FENCE ). I modelli rilassati cercano di catturare questa maggiore flessibilità degli ordini e preservare solo gli ordini che i programmatori “ richiedono"Per ottenere sia prestazioni più elevate che correttezza di SC. Ad esempio, in alcune architetture, i buffer di scrittura FIFO vengono utilizzati da ciascun core per conservare i risultati dei negozi impegnati (ritirati) prima di scrivere i risultati nella cache. Questa ottimizzazione migliora le prestazioni ma viola SC. Il buffer di scrittura nasconde la latenza della manutenzione di un archivio mancata. Poiché i negozi sono comuni, essere in grado di evitare lo stallo nella maggior parte di essi è un vantaggio importante. Per un processore single-core, un buffer di scrittura può essere reso architettonicamente invisibile assicurando che un carico sull'indirizzo A ritorni il valore dell'archivio più recente su A anche se uno o più archivi su A si trovano nel buffer di scrittura. Ciò avviene in genere bypassando il valore dell'archivio più recente su A al carico da A, dove "il più recente" è determinato dall'ordine del programma, oppure bloccando un carico di A se un archivio su A si trova nel buffer di scrittura. Quando vengono utilizzati più core, ognuno avrà il proprio buffer di scrittura di bypass. Senza buffer di scrittura, l'hardware è SC, ma con i buffer di scrittura non lo è, rendendo i buffer di scrittura architettonicamente visibili in un processore multicore.

Il riordino del negozio può verificarsi se un core ha un buffer di scrittura non FIFO che consente ai negozi di partire in un ordine diverso rispetto all'ordine in cui sono entrati. Ciò può verificarsi se il primo negozio manca nella cache mentre il secondo colpisce o se il secondo negozio può fondersi con un negozio precedente (cioè prima del primo negozio). Il riordino del caricamento del carico può anche verificarsi su core pianificati dinamicamente che eseguono istruzioni fuori dall'ordine del programma. Ciò può comportarsi come il riordino dei negozi su un altro core (puoi trovare un esempio di interleaving tra due thread?). Il riordino di un carico precedente con un archivio successivo (un riordino del magazzino di carico) può causare molti comportamenti errati, come il caricamento di un valore dopo aver rilasciato il blocco che lo protegge (se l'archivio è l'operazione di sblocco).

Poiché la coerenza della cache e la coerenza della memoria sono talvolta confuse, è istruttivo avere anche questa citazione:

A differenza della coerenza, la coerenza della cache non è né visibile al software né richiesta. La coerenza cerca di rendere le cache di un sistema a memoria condivisa funzionalmente invisibili come le cache in un sistema single-core. La corretta coerenza garantisce che un programmatore non possa determinare se e dove un sistema ha cache analizzando i risultati di carichi e depositi. Questo perché la corretta coerenza garantisce che le cache non abilitino mai nuovi o diversi comportamenti funzionali (i programmatori potrebbero comunque essere in grado di inferire la probabile struttura della cache usando i tempiinformazione). Lo scopo principale dei protocolli di coerenza della cache è mantenere invariante il single-writer-multiplo-lettori (SWMR) per ogni posizione di memoria. Una distinzione importante tra coerenza e coerenza è che la coerenza è specificata in base alla posizione della memoria , mentre la coerenza è specificata rispetto a tutte le posizioni della memoria.

Continuando con il nostro quadro mentale, l'invariante SWMR corrisponde al requisito fisico che ci sia al massimo una particella situata in una qualsiasi posizione, ma può esserci un numero illimitato di osservatori in qualsiasi posizione.


52
+1 per l'analogia con relatività speciale, ho provato a fare la stessa analogia da solo. Troppo spesso vedo programmatori che studiano il codice threaded cercando di interpretare il comportamento come operazioni in thread diversi che si verificano interlacciati tra loro in un ordine specifico e devo dire loro, no, con i sistemi multiprocessore la nozione di simultaneità tra diversi > i frame di riferimento </s> ora non hanno senso. Il confronto con la relatività speciale è un buon modo per far sì che rispettino la complessità del problema.
Pierre Lebeaupin,

71
Quindi dovresti concludere che l'Universo è multicore?
Peter K,

6
@PeterK: Esatto :) Ed ecco una bella visualizzazione di questa immagine del tempo da parte del fisico Brian Greene: youtube.com/watch?v=4BjGWLJNPcA&t=22m12s Questa è "The Illusion of Time [Full Documentary]" al minuto 22 e 12 secondi
Ahmed Nassar,

2
Sono solo io o sta passando da un modello di memoria 1D (asse orizzontale) a un modello di memoria 2D (piani di simultaneità). Lo trovo un po 'confuso, ma forse è perché non sono un madrelingua ... Ancora una lettura molto interessante.
Arrivederci SE

Hai dimenticato una parte essenziale: " analizzando i risultati di carichi e depositi " ... senza utilizzare informazioni precise sui tempi.
curioso

115

Questa è ormai una domanda di più anni, ma essendo molto popolare, vale la pena menzionare una fantastica risorsa per conoscere il modello di memoria C ++ 11. Non vedo il punto di riassumere il suo discorso per rendere questa ancora un'altra risposta completa, ma dato che questo è il ragazzo che ha effettivamente scritto lo standard, penso che valga la pena guardare il discorso.

Herb Sutter parla per tre ore del modello di memoria C ++ 11 intitolato "atomic <> Weapons", disponibile sul sito Channel9 - parte 1 e parte 2 . Il discorso è piuttosto tecnico e tratta i seguenti argomenti:

  1. Ottimizzazioni, gare e modello di memoria
  2. Ordine - Cosa: acquisire e rilasciare
  3. Ordinazione: come: mutex, atomica e / o recinzioni
  4. Altre restrizioni su compilatori e hardware
  5. Codice Gen & Performance: x86 / x64, IA64, POWER, ARM
  6. Atomics rilassato

Il discorso non si sviluppa sull'API, ma piuttosto sul ragionamento, sullo sfondo, sotto il cofano e dietro le quinte (sapevi che la semantica rilassata è stata aggiunta allo standard solo perché POWER e ARM non supportano il carico sincronizzato in modo efficiente?).


10
Quel discorso è davvero fantastico, vale davvero la pena le 3 ore che passerai a guardarlo.
ZunTzu,

5
@ZunTzu: sulla maggior parte dei lettori video è possibile impostare la velocità su 1,25, 1,5 o anche 2 volte l'originale.
Christian Severin,

4
@eran ragazzi avete le diapositive? i collegamenti nelle pagine di discussione del canale 9 non funzionano.
athos,

2
@athos non li ho, scusa. Prova a contattare il canale 9, non penso che la rimozione sia stata intenzionale (la mia ipotesi è che abbiano ottenuto il link da Herb Sutter, pubblicato così com'è, e in seguito ha rimosso i file; ma questa è solo una speculazione ...).
Eran,

75

Significa che lo standard ora definisce il multi-threading e definisce cosa succede nel contesto di più thread. Ovviamente, le persone hanno usato varie implementazioni, ma è come chiedersi perché dovremmo avere un std::stringmomento in cui tutti potremmo usare una stringclasse a casa.

Quando parli di thread POSIX o thread di Windows, allora questa è un po 'un'illusione dato che in realtà stai parlando di thread x86, poiché è una funzione hardware da eseguire contemporaneamente. Il modello di memoria C ++ 0x offre garanzie, sia su x86, che su ARM, o MIPS , o qualsiasi altra cosa tu possa trovare.


28
I thread Posix non sono limitati a x86. In effetti, i primi sistemi su cui sono stati implementati probabilmente non erano sistemi x86. I thread Posix sono indipendenti dal sistema e sono validi su tutte le piattaforme Posix. Inoltre, non è proprio vero che sia una proprietà hardware perché i thread Posix possono anche essere implementati attraverso il multitasking cooperativo. Ma ovviamente la maggior parte dei problemi di threading emergono solo dalle implementazioni di threading hardware (e alcuni anche solo su sistemi multiprocessore / multicore).
Celtschk,

57

Per le lingue che non specificano un modello di memoria, si sta scrivendo il codice per la lingua e il modello di memoria specificato dall'architettura del processore. Il processore può scegliere di riordinare gli accessi alla memoria per le prestazioni. Quindi, se il tuo programma ha corse di dati (una corsa di dati è quando è possibile che più core / hyper-thread accedano contemporaneamente alla stessa memoria), allora il tuo programma non è multipiattaforma a causa della sua dipendenza dal modello di memoria del processore. È possibile fare riferimento ai manuali del software Intel o AMD per scoprire come i processori possono riordinare gli accessi alla memoria.

Molto importante, i blocchi (e la semantica della concorrenza con blocco) sono in genere implementati in modo multipiattaforma ... Quindi, se si utilizzano i blocchi standard in un programma multithread senza corse di dati , non è necessario preoccuparsi dei modelli di memoria multipiattaforma .

È interessante notare che i compilatori Microsoft per C ++ hanno acquisito / rilasciato semantica per volatile che è un'estensione C ++ per far fronte alla mancanza di un modello di memoria in C ++ http://msdn.microsoft.com/en-us/library/12a04hfd(v=vs .80) .aspx . Tuttavia, dato che Windows funziona solo su x86 / x64, ciò non dice molto (i modelli di memoria Intel e AMD rendono facile ed efficiente implementare la semantica di acquisizione / rilascio in una lingua).


2
È vero che, quando è stata scritta la risposta, Windows funziona solo su x86 / x64, ma Windows funziona, ad un certo punto nel tempo, su IA64, MIPS, Alpha AXP64, PowerPC e ARM. Oggi funziona su varie versioni di ARM, che è una memoria abbastanza diversa dal x86, e da nessuna parte è altrettanto indulgente.
Lorenzo Dematté,

Quel collegamento è in qualche modo rotto (dice "Documentazione ritirata di Visual Studio 2005" ). Vuoi aggiornarlo?
Peter Mortensen,

3
Non era vero anche quando la risposta era scritta.
Ben

" accedere contemporaneamente alla stessa memoria " per accedere in modo conflittuale
curioso

27

Se usi i mutex per proteggere tutti i tuoi dati, non dovresti davvero preoccuparti. I mutex hanno sempre fornito sufficienti garanzie di ordinazione e visibilità.

Ora, se hai usato gli atomici o algoritmi senza lock, devi pensare al modello di memoria. Il modello di memoria descrive con precisione quando l'atomica fornisce ordini e garanzie di visibilità e fornisce recinzioni portatili per garanzie codificate a mano.

In precedenza, l'atomica veniva eseguita utilizzando le intrinseche del compilatore o una libreria di livello superiore. Le recinzioni sarebbero state eseguite utilizzando istruzioni specifiche della CPU (barriere di memoria).


19
Il problema prima era che non esisteva un mutex (in termini di standard C ++). Quindi le uniche garanzie che ti sono state fornite sono state dal produttore di mutex, che andava bene fintanto che non hai portato il codice (poiché le modifiche minori alle garanzie sono difficili da individuare). Ora riceviamo garanzie fornite dallo standard che dovrebbe essere portatile tra le piattaforme.
Martin York,

4
@Martin: in ogni caso, una cosa è il modello di memoria, e un'altra sono le atomiche e le primitive di threading che girano sopra quel modello di memoria.
ninjalj,

4
Inoltre, il mio punto era principalmente che in precedenza non esisteva per lo più un modello di memoria a livello di linguaggio, era il modello di memoria della CPU sottostante. Ora esiste un modello di memoria che fa parte del linguaggio principale; OTOH, mutex e simili potrebbero sempre essere fatti come una libreria.
ninjalj,

3
Potrebbe anche essere un vero problema per le persone che cercano di scrivere la libreria mutex. Quando la CPU, il controller di memoria, il kernel, il compilatore e la "libreria C" sono tutti implementati da diversi team, e alcuni di loro sono in disaccordo violento su come dovrebbe funzionare questa roba, beh, a volte roba noi programmatori di sistemi dobbiamo fare per presentare una bella facciata a livello di applicazioni non è affatto piacevole.
zwol,

11
Sfortunatamente non è sufficiente proteggere le strutture dei dati con semplici mutex se non esiste un modello di memoria coerente nella propria lingua. Esistono varie ottimizzazioni del compilatore che hanno senso in un singolo contesto di thread, ma quando entrano in gioco più thread e core della CPU, il riordino degli accessi alla memoria e altre ottimizzazioni possono produrre comportamenti indefiniti. Per ulteriori informazioni, consultare "I thread non possono essere implementati come libreria" di Hans Boehm: citeseer.ist.psu.edu/viewdoc/…
exDM69,

0

Le risposte di cui sopra ottengono gli aspetti più fondamentali del modello di memoria C ++. In pratica, la maggior parte degli usi del std::atomic<>"solo lavoro", almeno fino a quando il programmatore non ottimizza eccessivamente (ad esempio, cercando di rilassare troppe cose).

C'è un posto in cui gli errori sono ancora comuni: i blocchi di sequenza . C'è una discussione eccellente e di facile lettura delle sfide su https://www.hpl.hp.com/techreports/2012/HPL-2012-68.pdf . I blocchi di sequenza sono allettanti perché il lettore evita di scrivere sulla parola di blocco. Il codice seguente si basa sulla Figura 1 del rapporto tecnico sopra riportato ed evidenzia le sfide che si presentano quando si implementano i blocchi di sequenza in C ++:

atomic<uint64_t> seq; // seqlock representation
int data1, data2;     // this data will be protected by seq

T reader() {
    int r1, r2;
    unsigned seq0, seq1;
    while (true) {
        seq0 = seq;
        r1 = data1; // INCORRECT! Data Race!
        r2 = data2; // INCORRECT!
        seq1 = seq;

        // if the lock didn't change while I was reading, and
        // the lock wasn't held while I was reading, then my
        // reads should be valid
        if (seq0 == seq1 && !(seq0 & 1))
            break;
    }
    use(r1, r2);
}

void writer(int new_data1, int new_data2) {
    unsigned seq0 = seq;
    while (true) {
        if ((!(seq0 & 1)) && seq.compare_exchange_weak(seq0, seq0 + 1))
            break; // atomically moving the lock from even to odd is an acquire
    }
    data1 = new_data1;
    data2 = new_data2;
    seq = seq0 + 2; // release the lock by increasing its value to even
}

Inizialmente poco intuitivo data1e data2deve esserlo atomic<>. Se non sono atomici, potrebbero essere letti (in reader()) nello stesso momento in cui sono scritti (in writer()). Secondo il modello di memoria C ++, questa è una gara anche se reader()non utilizza mai effettivamente i dati . Inoltre, se non sono atomici, il compilatore può memorizzare nella cache la prima lettura di ciascun valore in un registro. Ovviamente non lo vorrai ... vuoi rileggere in ogni iterazione del whileloop in reader().

Inoltre, non è sufficiente crearli atomic<>e accedervi memory_order_relaxed. La ragione di ciò è che le letture di seq (in reader()) hanno acquisito solo la semantica. In termini semplici, se X e Y sono accessi alla memoria, X precede Y, X non è un'acquisizione o rilascio, e Y è un'acquisizione, quindi il compilatore può riordinare Y prima di X. Se Y era la seconda lettura di seq e X era una lettura di dati, un tale riordino avrebbe interrotto l'implementazione del blocco.

Il documento offre alcune soluzioni. Quello con le migliori prestazioni oggi è probabilmente quello che usa un atomic_thread_fencecon memory_order_relaxed prima della seconda lettura del seqlock. Nel documento, è la Figura 6. Non sto riproducendo il codice qui, perché chiunque abbia letto così lontano dovrebbe davvero leggere il documento. È più preciso e completo di questo post.

L'ultimo problema è che potrebbe essere innaturale rendere dataatomiche le variabili. Se non riesci a inserire il tuo codice, devi fare molta attenzione, perché il casting da non atomico a atomico è legale solo per i tipi primitivi. Dovrebbe essere aggiunto C ++ 20 atomic_ref<>, il che renderà più facile risolvere questo problema.

Riassumendo: anche se pensi di aver compreso il modello di memoria C ++, dovresti stare molto attento prima di lanciare i tuoi blocchi di sequenza.


-2

C e C ++ erano definiti da una traccia di esecuzione di un programma ben formato.

Ora sono per metà definiti da una traccia di esecuzione di un programma e per metà a posteriori da molti ordini su oggetti di sincronizzazione.

Significa che queste definizioni linguistiche non hanno alcun senso in quanto non esiste un metodo logico per mescolare questi due approcci. In particolare, la distruzione di un mutex o di una variabile atomica non è ben definita.


Condivido il tuo feroce desiderio di migliorare il design del linguaggio, ma penso che la tua risposta sarebbe più preziosa se fosse centrata su un caso semplice, per il quale hai mostrato chiaramente ed esplicitamente come quel comportamento viola specifici principi di design del linguaggio. Dopodiché ti consiglio caldamente, se mi permetti, di dare in quella risposta un ottimo argomento per la pertinenza di ciascuno di quei punti, perché saranno contrastati dalla rilevanza degli immensi benefici di produttività percepiti dal design C ++
Matias Haeussler,

1
@MatiasHaeussler Penso che tu abbia letto male la mia risposta; Non sto obiettando alla definizione di una particolare funzionalità C ++ qui (ho anche molte critiche puntate ma non qui). Sto sostenendo che non esiste un costrutto ben definito in C ++ (né C). L'intera semantica MT è un disastro completo, poiché non hai più semantica sequenziale. (Credo che Java MT sia rotto ma meno.) Il "semplice esempio" sarebbe quasi qualsiasi programma MT. Se non sei d'accordo, sei il benvenuto a rispondere alla mia domanda su come dimostrare la correttezza dei programmi MT C ++ .
curioso

Interessante, penso di aver capito di più cosa intendi dopo aver letto la tua domanda. Se ho ragione, ti riferisci all'impossibilità di sviluppare prove per la correttezza dei programmi C ++ MT . In tal caso, direi che per me è qualcosa di enorme importanza per il futuro della programmazione informatica, in particolare per l'arrivo dell'intelligenza artificiale. Ma vorrei anche sottolineare che per la stragrande maggioranza delle persone che fanno domande in overflow dello stack non è qualcosa di cui sono nemmeno a conoscenza, e anche dopo aver capito cosa intendi e diventando interessati
Matias Haeussler

1
"Le domande sulla dimostrabilità dei programmi per computer devono essere pubblicate in stackoverflow o in stackexchange (se in nessuno dei due, dove)?" Questo sembra essere uno per meta stackoverflow, non è vero?
Matias Haeussler,

1
@MatiasHaeussler 1) C e C ++ condividono essenzialmente il "modello di memoria" di variabili atomiche, mutex e multithreading. 2) La rilevanza al riguardo riguarda i vantaggi di avere il "modello di memoria". Penso che il vantaggio sia zero poiché il modello non è fondato.
curioso
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.