2013-10-17 14 views
5

AFAIK C++ Atomics (<atomic>) rodzina dostarczyć 3 zalety:C++ Atomics i widoczność cross-gwint

  • prymitywny instrukcji niepodzielność (nie brudny czyta)
  • zamawiania pamięci (zarówno dla CPU i kompilatora) oraz
  • Przejrzystość widoczności/zmiany poprzeczne.

I nie jestem pewien co do trzeciej kuli, więc spójrz na poniższy przykład.

#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; 
} 

Najpierw proszę potwierdzić moje założenia w kodzie (szczególnie thread2).

Po drugie, moje pytania są następujące:

  1. jaki sposób a_flag napisać rozprzestrzeniać się do innych rdzeni?

  2. Czy std::atomic synchronizuje a_flag w pamięci podręcznej programu piszącego z inną pamięcią podręczną rdzeni (przy użyciu MESI lub cokolwiek innego), czy propagacja jest automatyczna?

  3. Zakładając, że na konkretnym komputerze zapis na flagę jest atomowy (pomyśl int_32 na x86) A my nie mamy pamięci prywatnej do zsynchronizowania (mamy tylko flagę) czy potrzebujemy atomów?

  4. Biorąc pod uwagę najbardziej popularnych architektur procesorów (x86, x64, ARM v.whatever, IA-64), jest widoczność przekroju rdzenia (jestem teraz nie rozważają reorderings) automatyczne (ale potencjalnie opóźnione), lub musisz wydać konkretne polecenia, aby propagować dowolne dane?

Odpowiedz

2
  1. Rdzenie sami nie mają znaczenia. Pytanie brzmi: "w jaki sposób wszystkie rdzenie widzą w końcu tę samą aktualizację pamięci", co jest czymś, co twój sprzęt robi dla ciebie (np. Protokoły koherencji pamięci podręcznej). Jest tylko jedna pamięć, więc głównym problemem jest buforowanie, które jest prywatnym problemem sprzętu.

  2. To pytanie wydaje się niejasne. Liczy się para nabycie-uwolnienie utworzone przez ładunek i magazyn a_flag, który jest punktem synchronizacji i powoduje, że efekty thread0 i thread1 pojawiają się w określonej kolejności (tj. Wszystko dzieje się w thread0, zanim stanie się sklep - przed wszystkim po pętli w thread1).

  3. Tak, w przeciwnym razie nie miałbyś punktu synchronizacji.

  4. Nie potrzebujesz żadnych "poleceń" w C++. C++ nie jest nawet świadomy faktu, że działa na jakimś konkretnym rodzaju procesora. Prawdopodobnie mógłbyś uruchomić program C++ na kostce Rubika z wystarczającą wyobraźnią. Kompilator C++ wybiera niezbędne instrukcje, aby zaimplementować zachowanie synchronizacji opisane przez model pamięci C++, a także na x86, który obejmuje wydawanie prefiksów blokad instrukcji i ogrodzeń pamięci, a także zbytnie niepotrzebne zamawianie instrukcji.Ponieważ x86 ma mocno uporządkowany model pamięci, powyższy kod powinien generować minimalny dodatkowy kod w porównaniu do naiwnego, niepoprawnego bez atomu.

  5. Posiadanie kodu thread2 powoduje, że cały program pozostaje niezdefiniowanym zachowaniem.


Tylko dla zabawy, i aby pokazać, że pracuje się, co się dzieje dla siebie może być budujące, skompilowany kod w trzech wariantach. (Dodałem glbbal int x iw thread1 dodałem x = data.y;).

Acquire/Release: (kod)

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 

Kolejno spójne: (usunąć wyraźne zamawiania)

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": (tylko przy użyciu 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 

Jak widać, nie ma dużej różnicy. "Niepoprawna" wersja wygląda w większości poprawnie, z wyjątkiem braku ładunku (korzysta z cmp z operandem pamięci). Sekwencyjnie spójna wersja ukrywa swoją kosztowność w instrukcji xcgh, która ma ukryty przedrostek blokujący i nie wymaga żadnych wyraźnych przeszkód.