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 0
perché 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 0
come 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 0
non è 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 0
o 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 .