Zamawianie modeli pamięci i widoczność?

Próbowałem szukać szczegółów na ten temat, czytałem nawet standard o muteksach i atomice... ale nadal nie mogłem zrozumieć gwarancji widoczności modelu pamięci C++11. Z tego, co rozumiem, bardzo ważną cechą mutex obok wzajemnego wykluczenia jest zapewnienie widoczności. Aka nie wystarczy, że tylko jeden wątek na raz zwiększa licznik, ważne jest, że wątek zwiększa licznik, który był przechowywany przez wątek, który był ostatnio za pomocą mutex(naprawdę Nie wiem dlaczego ludzie nie wspominają o tym więcej przy omawianiu muteksów, może miałem złych nauczycieli :)). Więc z tego co wiem, atomic nie wymusza natychmiastowej widoczności: (od osoby, która prowadzi wątek boost:: i zaimplementowała wątek c++11 i bibliotekę mutex):

Ogrodzenie z memory_order_seq_cst nie wymusza natychmiastowego widoczność do innych wątków (podobnie jak instrukcja MFENCE). Ograniczenia zamawiania pamięci C++0x są po prostu takie - - - zamawianie ograniczenia. operacje memory_order_seq_cst tworzą całkowity porządek, ale nie ma żadnych ograniczeń co do tego, czym jest to zamówienie, poza tym, że musi być uzgodnione przez wszystkie wątki i nie może naruszać innych zamówień ograniczenia. W szczególności wątki mogą nadal widzieć wartości "stare" przez jakiś czas, pod warunkiem, że widzą wartości w kolejności zgodnej z ograniczenia.

Nie przeszkadza mi to. Ale problem polega na tym, że mam problem ze zrozumieniem czym są konstrukcje C++11 dotyczące atomic "globalne" i które zapewniają spójność tylko w przypadku zmiennych atomowych. W szczególności mam zrozumienie, które (jeśli w ogóle) z następujących kolejności pamięci gwarantują, że będzie płot pamięci przed i po załadowaniu i przechowuje: http://www.stdthread.co.uk/doc/headers/atomic/memory_order.html

Z tego co mogę powiedzieć std::memory_order_seq_cst wstawia barierę mem, podczas gdy inne wymuszają tylko kolejność operacji w określonym miejscu pamięci.

Więc może ktoś to wyczyścić domyślam się, że wiele osób będzie robić straszne błędy używając std:: atomic, esp jeśli nie używają domyślnego (STD::memory_order_seq_cst memory ordering)
2. jeśli mam rację to znaczy, że druga linia jest redundand w tym kodzie:

atomicVar.store(42);
std::atomic_thread_fence(std::memory_order_seq_cst);  

3. do std:: atomic_thread_fences mają te same wymagania co mutexy w tym sensie, że aby zapewnić spójność seq na nieatomowych var-ach, należy wykonać STD:: atomic_thread_fence(STD::memory_order_seq_cst); przed załadowaniem i std:: atomic_thread_fence (std:: memory_order_seq_cst);
po sklepach?
4. Is

  {
    regularSum+=atomicVar.load();
    regularVar1++;
    regularVar2++;
    }
    //...
    {
    regularVar1++;
    regularVar2++;
    atomicVar.store(74656);
  }

Odpowiednik

std::mutex mtx;
{
   std::unique_lock<std::mutex> ul(mtx);
   sum+=nowRegularVar;
   regularVar++;
   regularVar2++;
}
//..
{
   std::unique_lock<std::mutex> ul(mtx);
    regularVar1++;
    regularVar2++;
    nowRegularVar=(74656);
}
Nie sądzę, ale chciałbym się upewnić.

Edytuj: 5. Czy można prowadzić ogień?
Istnieją tylko dwa wątki.

atomic<int*> p=nullptr; 

Pierwszy wątek pisze

{
    nonatomic_p=(int*) malloc(16*1024*sizeof(int));
    for(int i=0;i<16*1024;++i)
    nonatomic_p[i]=42;
    p=nonatomic;
}

Drugi wątek czyta

{
    while (p==nullptr)
    {
    }
    assert(p[1234]==42);//1234-random idx in array
}
Author: Xeo, 2011-09-18

2 answers

Jeśli lubisz zajmować się ogrodzeniami, to {[2] } jest równoważne a.load(memory_order_relaxed), a następnie atomic_thread_fence(memory_order_acquire). Podobnie, {[5] } jest równoważne wywołaniu do atomic_thread_fence(memory_order_release) przed wywołaniem do a.store(x,memory_order_relaxed). memory_order_consume jest szczególnym przypadkiem memory_order_acquire, tylko dla danych zależnych . memory_order_seq_cst jest specjalna i tworzy całkowity porządek we wszystkich operacjach memory_order_seq_cst. W połączeniu z innymi jest to to samo, co nabycie dla ładunku, a zwolnienie dla sklepu. memory_order_acq_rel jest dla operacji read-modify-write I jest odpowiednikiem acquire na części read I wydanie na stronie RMW.

Użycie ograniczeń porządkowania operacji atomowych może, ale nie musi, skutkować rzeczywistymi instrukcjami ogrodzenia, w zależności od architektury sprzętowej. W niektórych przypadkach kompilator wygeneruje lepszy kod, jeśli ograniczysz kolejność operacji atomowych, zamiast używać oddzielnego ogrodzenia.

Na x86 ładunki są zawsze nabywane, a sklepy są zawsze zwalniane. memory_order_seq_cst wymaga silniejszego zamawiania z MFENCE instrukcją lub LOCK Instrukcja prefiksowa (jest tu wybór implementacji, czy sprawić, by Sklep miał mocniejsze zamawianie czy ładowanie). W związku z tym samodzielne ogrodzenia pozyskania i uwolnienia nie są ops, ale atomic_thread_fence(memory_order_seq_cst) nie jest (ponownie wymaga MFENCElub LOCK instrukcji ed).

Ważnym efektem ograniczeń porządkowania jest to, że zlecają one inne operacje .

std::atomic<bool> ready(false);
int i=0;

void thread_1()
{
    i=42;
    ready.store(true,memory_order_release);
}

void thread_2()
{
    while(!ready.load(memory_order_acquire)) std::this_thread::yield();
    assert(i==42);
}

thread_2 obraca się, aż odczytuje true z ready. Ponieważ sklep do ready w thread_1 jest release, a load jest acquire następnie store synchronizuje-z load, a store do i happens-before the load from i in the assert, and the assert will not fire.

2) Druga linia w

atomicVar.store(42);
std::atomic_thread_fence(std::memory_order_seq_cst);  

Jest rzeczywiście potencjalnie zbędny, ponieważ sklep do atomicVar domyślnie używa memory_order_seq_cst. Jeśli jednak istnieją inne operacje niememory_order_seq_cst atomowe na tym wątku, ogrodzenie może mieć konsekwencje. Przykładowo, działałoby jako płot zwalniający dla kolejnego a.store(x,memory_order_relaxed).

[33]}3) ogrodzenia i operacje atomowe nie działają jak muteksy. Możesz ich używać do budowania muteksów, ale nie działają tak jak one. Nie musisz nigdy używać atomic_thread_fence(memory_order_seq_cst). Nie ma wymogu, aby jakiekolwiek operacje atomowe były memory_order_seq_cst, a uporządkowanie na zmiennych nieatomowych można osiągnąć bez, jak w powyższym przykładzie.

4) nie są one równoważne. Twój Fragment bez blokady mutex jest więc rasą danych i niezdefiniowany zachowanie.

5) nie twoje twierdzenie nie może strzelać. Przy domyślnej kolejności pamięci memory_order_seq_cst, store I load ze wskaźnika atomowego p działają jak store I load w moim przykładzie powyżej, a Magazyny do elementów tablicy są gwarantowane-przed odczytaniem.

 23
Author: Anthony Williams,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-10-19 16:54:50

Z tego co mogę powiedzieć std::memory_order_seq_cst wstawia barierę mem, podczas gdy inne wymuszają tylko kolejność operacji w określonym miejscu pamięci.

To naprawdę zależy od tego, co robisz i na jakiej platformie pracujesz. Silny model zamawiania pamięci na platformie takiej jak x86 stworzy inny zestaw wymagań dotyczących istnienia operacji ogrodzenia pamięci w porównaniu do słabszego modelu zamawiania na platformach takich jak IA64, PowerPC, ARM itp. Co do ... domyślnym parametrem std::memory_order_seq_cst jest zapewnienie, że w zależności od platformy zostaną użyte odpowiednie instrukcje ogrodzenia pamięci. Na platformie takiej jak x86 nie ma potrzeby stosowania pełnej bariery pamięci, chyba że wykonujesz operację odczytu-modyfikacji-zapisu. W modelu pamięci x86 wszystkie ładunki mają semantykę load-acquire, a wszystkie sklepy mają semantykę store-release. Tak więc, w tych przypadkach std::memory_order_seq_cst enum zasadniczo tworzy no-op, ponieważ model pamięci dla x86 już zapewnia, że tego typu operacje są spójne we wszystkich wątkach, dlatego nie ma instrukcji montażu, które implementują tego typu bariery pamięci częściowej. Tak więc ten sam warunek no-op byłby prawdziwy, jeśli jawnie ustawisz std::memory_order_release lub std::memory_order_acquire ustawienie na x86. Ponadto Wymaganie pełnej bariery pamięci w takich sytuacjach byłoby niepotrzebną przeszkodą w wydajności. Jak wspomniano, będzie on wymagany tylko do operacji read-modify-store.

Na innych platformach ze słabszymi modelami spójności pamięci, nie byłoby tak, dlatego użycie std::memory_order_seq_cst wykorzystywałoby odpowiednie operacje ogrodzenia pamięci bez konieczności wyraźnego określania, czy użytkownik chciałby operacji Load-acquire, store-release czy full memory fence. Platformy te mają specjalne instrukcje maszynowe do egzekwowania takich umów spójności pamięci, a ustawienie std::memory_order_seq_cst rozwiązałoby właściwy przypadek. Jeśli użytkownik chce konkretnie wywołać jedną z tych operacji, może za pomocą jawnego std::memory_order typy enum, ale nie byłoby to konieczne ... kompilator wypracowałby poprawne ustawienia.

Zakładam, że wiele osób będzie robić straszne błędy używając std:: atomic, esp jeśli nie używają domyślnego (STD::memory_order_seq_cst memory ordering)

Tak, jeśli nie wiedzą, co robią i nie rozumieją, jakie typy semantyki bariery pamięci są wymagane w pewnych operacjach, wtedy będzie wiele błędów, jeśli spróbują wyraźnie określić rodzaj bariery pamięci i jest to niepoprawna, zwłaszcza na platformach, które nie pomogą im w złym zrozumieniu kolejności pamięci, ponieważ są słabsze w naturze.

Na koniec pamiętaj o swojej sytuacji # 4 dotyczącej mutex, że tutaj muszą się wydarzyć dwie różne rzeczy:

  1. kompilator nie może mieć możliwości zmiany kolejności operacji w sekcji mutex i critical (szczególnie w przypadku kompilatora optymalizującego)
  2. muszą istnieć wymagane ogrodzenia pamięci (w zależności od platformy), które utrzymują stan, w którym wszystkie sklepy są zakończone przed sekcją krytyczną i odczytem zmiennej mutex, a wszystkie sklepy są zakończone przed opuszczeniem sekcji krytycznej.

Ponieważ domyślnie, atomowe magazyny i ładunki są zaimplementowane za pomocą std::memory_order_seq_cst, użycie atomiki zaimplementowałoby również odpowiednie mechanizmy spełniające warunki #1 i #2. To powiedziawszy, w Twoim pierwszym przykładzie w przypadku atomics obciążenie wymusi semantykę acquire-dla bloku, podczas gdy store wymusi semantykę release. Nie wymusiłoby to jednak żadnego konkretnego porządku wewnątrz "sekcji krytycznej" między tymi dwoma operacjami. W drugim przykładzie masz dwie różne sekcje z blokadami, z których każda posiada semantykę. Ponieważ w pewnym momencie trzeba by zwolnić blokady, które mają semantykę Wydania, to nie, dwa bloki kodu nie będą równoważne. W w pierwszym przykładzie utworzyłeś dużą "sekcję krytyczną" pomiędzy load I store (zakładając, że wszystko dzieje się w tym samym wątku). W drugim przykładzie masz dwie różne sekcje krytyczne.

P. S. znalazłem następujący PDF szczególnie pouczający, i może go również znajdziesz: http://www.nwcpp.org/Downloads/2008/Memory_Fences.pdf

 7
Author: Jason,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-09-19 14:57:49