2013-10-17 1 views
5

AFAIK C++ atomics (<atomic>) -Familie bieten 3 Vorteile:C++ atomics und verkanten Sichtbarkeits

  • primitive Befehle Unteilbarkeit (kein schmutziges liest),
  • Speicherordnung (beide, für die CPU und Compiler) und
  • Cross-Thread-Sichtbarkeit/Änderungen Ausbreitung.

Und ich bin mir nicht sicher über die dritte Kugel, werfen Sie einen Blick auf das folgende Beispiel.

#include <atomic> 

std::atomic_bool a_flag = ATOMIC_VAR_INIT(false); 
struct Data { 
    int x; 
    long long y; 
    char const* z; 
} data; 

void thread0() 
{ 
    // due to "release" the data will be written to memory 
    // exactly in the following order: x -> y -> z 
    data.x = 1; 
    data.y = 100; 
    data.z = "foo"; 
    // there can be an arbitrary delay between the write 
    // to any of the members and it's visibility in other 
    // threads (which don't synchronize explicitly) 

    // atomic_bool guarantees that the write to the "a_flag" 
    // will be clean, thus no other thread will ever read some 
    // strange mixture of 4bit + 4bits 
    a_flag.store(true, std::memory_order_release); 
} 

void thread1() 
{ 
    while (a_flag.load(std::memory_order_acquire) == false) {}; 
    // "acquire" on a "released" atomic guarantees that all the writes from 
    // thread0 (thus data members modification) will be visible here 
} 

void thread2() 
{ 
    while (data.y != 100) {}; 
    // not "acquiring" the "a_flag" doesn't guarantee that will see all the 
    // memory writes, but when I see the z == 100 I know I can assume that 
    // prior writes have been done due to "release ordering" => assert(x == 1) 
} 

int main() 
{ 
    thread0(); // concurrently 
    thread1(); // concurrently 
    thread2(); // concurrently 

    // join 

    return 0; 
} 

Zuerst, bitte überprüfen Sie meine Annahmen in Code (insbesondere thread2).

Zweitens meine Fragen sind:

  1. Wie funktioniert die a_flag auf andere Kerne ausbreiten schreiben?

  2. Synchronisiert die std::atomic die a_flag im Writer-Cache mit dem anderen Kerne-Cache (mit MESI oder irgendetwas anderes), oder die Propagierung ist automatisch?

  3. Angenommen, auf einer bestimmten Maschine ist ein Schreiben zu einem Flag atomar (think int_32 auf x86) Und wir haben keinen privaten Speicher zu synchronisieren (wir haben nur ein Flag) müssen wir Atomics verwenden?

  4. Unter Berücksichtigung beliebtestene CPU-Architekturen (x86, x64, ARM v.whatever, IA-64), ist die Quer Kern Sicht (ich bin jetzt nicht Berücksichtigung Umordnungen) automatisch (aber möglicherweise verzögert), Oder müssen Sie bestimmte Befehle ausgeben, um Daten zu verbreiten?

Antwort

2
  1. Kerne selbst keine Rolle spielen. Die Frage ist, "wie sehen alle Kerne das gleiche Speicher-Update schließlich", was etwas ist, was Ihre Hardware für Sie tut (z. B. Cache-Kohärenz-Protokolle). Da es nur einen Speicher gibt, geht es hauptsächlich um das Caching, was ein privates Anliegen der Hardware ist.

  2. Diese Frage scheint unklar. Was zählt, ist das Übernahme-Freigabe-Paar, gebildet durch Laden und Speichern von a_flag, welches ein Synchronisationspunkt ist und bewirkt, dass die Effekte thread0 und in einer bestimmten Reihenfolge erscheinen (dh alles in thread0 vor dem Laden passiert-vor alles nach der Schleife in thread1).

  3. Ja, sonst hätten Sie keinen Synchronisationspunkt.

  4. Sie brauchen keine "Befehle" in C++. C++ ist sich nicht einmal bewusst, dass es auf einer bestimmten Art von CPU läuft. Sie könnten wahrscheinlich ein C++ - Programm auf einem Zauberwürfel mit genügend Phantasie ausführen. Ein C++ Compiler wählt die erforderlichen Anweisungen aus, um das Synchronisierungsverhalten zu implementieren, das durch das C++ - Speichermodell beschrieben wird, und auf x86, das das Ausgeben von Befehlssperr-Präfixen und Speicher-Zäunen sowie das Nicht-Neuordnen von Anweisungen enthält.Da x86 ein stark geordnetes Speichermodell hat, sollte der obige Code minimalen zusätzlichen Code im Vergleich zu dem naiven, inkorrekten Code ohne Atomics erzeugen.

  5. Wenn Sie thread2 im Code haben, wird das gesamte Programm undefiniert.


Just for fun, und um zu zeigen, dass die Arbeit aus, was passiert, für sich selbst erbaulich sein kann, ich den Code in drei Varianten zusammengestellt. (Ich fügte ein glbbal int x hinzu und in thread1 fügte ich x = data.y; hinzu).

Acquire/Release: (Code)

thread0: 
    mov DWORD PTR data, 1 
    mov DWORD PTR data+4, 100 
    mov DWORD PTR data+8, 0 
    mov DWORD PTR data+12, OFFSET FLAT:.LC0 
    mov BYTE PTR a_flag, 1 
    ret 

thread1: 
.L14: 
    movzx eax, BYTE PTR a_flag 
    test al, al 
    je .L14 
    mov eax, DWORD PTR data+4 
    mov DWORD PTR x, eax 
    ret 

Sequenziell konsequent: (Entfernen der ausdrückliche Bestellung)

thread0: 
    mov eax, 1 
    mov DWORD PTR data, 1 
    mov DWORD PTR data+4, 100 
    mov DWORD PTR data+8, 0 
    mov DWORD PTR data+12, OFFSET FLAT:.LC0 
    xchg al, BYTE PTR a_flag 
    ret 

thread1: 
.L14: 
    movzx eax, BYTE PTR a_flag 
    test al, al 
    je .L14 
    mov eax, DWORD PTR data+4 
    mov DWORD PTR x, eax 
    ret 

"Naive": (nur mit bool)

thread0: 
    mov DWORD PTR data, 1 
    mov DWORD PTR data+4, 100 
    mov DWORD PTR data+8, 0 
    mov DWORD PTR data+12, OFFSET FLAT:.LC0 
    mov BYTE PTR a_flag, 1 
    ret 

thread1: 
    cmp BYTE PTR a_flag, 0 
    jne .L3 
.L4: 
    jmp .L4 
.L3: 
    mov eax, DWORD PTR data+4 
    mov DWORD PTR x, eax 
    ret 

Wie Sie sehen können, gibt es keinen großen Unterschied. Die "falsche" Version sieht tatsächlich größtenteils korrekt aus, außer dass die Last fehlt (sie verwendet cmp mit Speicheroperand). Die sequentiell konsistente Version verbirgt ihre Teuerheit in der Anweisung , die ein implizites Sperrpräfix hat und anscheinend keine expliziten Zäune benötigt.