Atomicità in C ++: mito o realtà

Ho letto un articolo su Lockless Programming in MSDN. Dice :

Su tutti i processori moderni, si può presumere che le letture e le scritture di tipi nativi allineati in modo naturale siano atomici . Finché il bus di memoria è largo almeno quanto il tipo letto o scritto, la CPU legge e scrive questi tipi in una singola transazione bus, rendendo imansible per gli altri thread vederli in uno stato incompleto.

E fornisce alcuni esempi:

// This write is not atomic because it is not natively aligned. DWORD* pData = (DWORD*)(pChar + 1); *pData = 0; // This is not atomic because it is three separate operations. ++g_globalCounter; // This write is atomic. g_alignedGlobal = 0; // This read is atomic. DWORD local = g_alignedGlobal; 

Ho letto molte risposte e commenti dicendo che nulla è garantito per essere atomico in C ++ e non è nemmeno menzionato negli standar, in SO e ora sono un po ‘confuso. Sto interpretando male l’articolo? O lo scrittore dell’articolo parla di cose non standard e specifiche del compilatore MSVC ++?

Quindi, secondo l’articolo, i seguenti compiti devono essere atomici, giusto?

     struct Data { char ID; char pad1[3]; short Number; char pad2[2]; char Name[5]; char pad3[3]; int Number2; double Value; } DataVal; DataVal.ID = 0; DataVal.Number = 1000; DataVal.Number2 = 0xFFFFFF; DataVal.Value = 1.2; 

    Se è vero, sostituisce Name[5] e pad3[3] con std::string Name; fare qualche differenza nell’allineamento della memoria? Gli assegnamenti alle variabili Number2 e Value saranno ancora atomici?

    Qualcuno può spiegare per favore?

    Questa raccomandazione è specifica per l’architettura. È vero per x86 e x86_64 (in una programmazione di basso livello). Dovresti anche controllare che il compilatore non riordini il tuo codice. È ansible utilizzare “barriera di memoria del compilatore” per questo.

    Le letture e le scritture atomiche a basso livello per x86 sono descritte nei manuali di riferimento di Intel “Il Manuale dello sviluppatore del software per le architetture Intel® 64 e IA-32” Volume 3A ( http://www.intel.com/Assets/PDF/manual/253668. pdf ), sezione 8.1.1

    8.1.1 Operazioni atomiche garantite

    Il processore Intel486 (e più recenti i processori da) garantisce che le seguenti operazioni di base della memoria saranno sempre eseguite atomicamente:

    • Leggere o scrivere un byte
    • Leggere o scrivere una parola allineata su un limite di 16 bit
    • Lettura o scrittura di una doppia parola allineata su un limite a 32 bit

    Il processore Pentium (e più recenti i processori da) garantisce che le seguenti operazioni di memoria aggiuntive saranno sempre eseguite atomicamente:

    • Lettura o scrittura di una quadrupla allineata su un limite a 64 bit
    • Accesso a 16 bit a posizioni di memoria non memorizzate che si adattano a un bus dati a 32 bit

    I processori della famiglia P6 (e più recenti i processori da) garantiscono che la seguente operazione di memoria aggiuntiva verrà sempre eseguita atomicamente:

    • Accesso non allineato a 16, 32 e 64 bit alla memoria memorizzata nella cache che si adatta a una riga della cache

    Questo documento ha anche una descrizione più atomica per i processori più recenti come Core2. Non tutte le operazioni non allineate saranno atomiche.

    Altro manuale di Intel consiglia questo white paper:

    http://software.intel.com/en-us/articles/developing-multithreaded-applications-a-platform-consistent-approach/

    Penso che tu stia interpretando male la citazione.

    L’atomicità può essere garantita su una determinata architettura, utilizzando istruzioni specifiche (proprie di questa architettura). L’articolo MSDN spiega che leggere e scrivere sui tipi incorporati di C ++ può essere considerato atomico su architettura x86 .

    Tuttavia lo standard C ++ non presuppone quale sia l’architettura, quindi lo Standard non può fornire tali garanzie. Infatti, il C ++ è utilizzato in software embedded in cui il supporto hardware è molto più limitato.

    C ++ 0x definisce la class template std::atomic , che consente di trasformare letture e scritture in operazioni atomiche , qualunque sia il tipo. Il compilatore selezionerà il modo migliore per ottenere l’atomicità in base alle caratteristiche del tipo e all’architettura mirata in modo conforms allo standard.

    Il nuovo standard definisce anche un sacco di operazioni simili a MSVC InterlockExchange che è anche compilato con le primitive disponibili più veloci (ma al sicuro) offerte dall’hardware.

    Lo standard c ++ non garantisce il comportamento atomico. In pratica, tuttavia, le operazioni di caricamento e archiviazione semplici saranno atomiche, come afferma l’articolo.

    Se hai bisogno di atomicità, meglio essere esplicito su di esso e utilizzare una sorta di blocco però.

     *counter = 0; // this is atomic on most platforms *counter++; // this is NOT atomic on most platforms 

    Fai molta attenzione quando fai affidamento sull’atomicità delle operazioni con parole semplici perché le cose potrebbero comportarsi in modo diverso da come ti aspetti. Su architetture multicore, potresti assistere a letture e scritture fuori servizio. Ciò richiederà quindi barriere di memoria per prevenire. (maggiori dettagli qui ).

    La linea di fondo per uno sviluppatore di applicazioni è l’uso di primitive che le garanzie del sistema operativo saranno atomiche o utilizzare i blocchi appropriati.

    IMO, l’articolo incorpora alcune ipotesi sull’architettura sottostante. Dato che il C ++ ha solo alcuni requisiti minimalisti sull’architettura, non si possono fornire garanzie ad esempio sull’atomicità nello standard. Ad esempio un byte deve essere di almeno 8 bit, ma si potrebbe avere un’architettura in cui un byte è 9 bit, ma un int 16 … teoricamente.

    Pertanto, quando il compilatore è specifico per l’architettura x86, è ansible utilizzare le funzionalità specifiche.

    NB: le strutture sono solitamente allineate per impostazione predefinita a un limite di parole nativo. puoi disabilitarlo con le istruzioni #pragma, quindi i riempimenti del padding non sono richiesti

    Penso che quello che stanno cercando di ottenere, sia che i tipi di dati implementati in modo nativo dall’hardware, vengono aggiornati all’interno dell’hardware in modo tale che la lettura da un altro thread non ti darà mai un valore aggiornato parzialmente.

    Considera un numero intero a 32 bit su una macchina a 32 bit. È scritto o letto completamente in un ciclo di istruzioni, mentre i tipi di dati di dimensioni maggiori, ad esempio un int di 64 bit su una macchina a 32 bit richiederà più cicli, quindi teoricamente il thread che li scrive potrebbe essere interrotto tra quei cicli. non in uno stato valido.

    Nessuna stringa di utilizzo non lo renderebbe atomico, poiché la stringa è un costrutto di livello superiore e non implementata nell’hardware. Modifica: Come per il tuo commento su cosa intendi (non intendi) sul passaggio alla stringa, non dovrebbe fare alcuna differenza per i campi dichiarati dopo, come menzionato in un’altra risposta il compilatore allineerà i campi per impostazione predefinita.

    La ragione per cui non è nello standard è che, come affermato nell’articolo, si tratta di come i moderni processori implementano le istruzioni. Il tuo codice C / C ++ standard dovrebbe funzionare esattamente allo stesso modo su una macchina a 16 o 64 bit (solo con differenza di prestazioni), tuttavia se si assume che si eseguirà solo su una macchina a 64 bit, qualsiasi cosa a 64 bit o più piccola è atomica. (Tipo SSE ecc. A parte)

    Penso che l’ atomicity come viene descritta nell’articolo abbia un uso limitato. Ciò significa che si leggerà / scriverà un valore valido ma probabilmente obsoleto. Quindi leggendo un int, lo leggerete completamente, non 2 byte da un vecchio valore e altri 2 byte da un nuovo valore attualmente scritto da un altro thread.

    Ciò che è importante per la memoria condivisa sono le barriere della memoria. E sono garantiti da primitive di sincronizzazione come atomic tipi atomic C ++ 0x, i mutexes , ecc.

    Non penso che il cambiamento di char Name[5] in std::string Name farà la differenza se lo si utilizza solo per le assegnazioni di singoli caratteri , poiché l’operatore indice restituirà un riferimento diretto al carattere sottostante. Un assegnamento di stringa completo non è atomico (e non puoi farlo con un array di caratteri, quindi suppongo che tu non stavi pensando di usarlo in questo modo comunque).