Co gwarantuje C ++ std :: atomic na poziomie programisty?

9

Wysłuchałem i przeczytałem kilka artykułów, rozmów i pytań o przepełnienie stosu std::atomici chciałbym mieć pewność, że dobrze to zrozumiałem. Ponieważ nadal jestem trochę mylony z linią pamięci podręcznej zapisuje widoczność z powodu możliwych opóźnień w protokołach koherencji pamięci podręcznej MESI (lub pochodnych), buforach pamięci, unieważnianiu kolejek i tak dalej.

Przeczytałem, że x86 ma silniejszy model pamięci i że jeśli opóźnienie unieważnienia pamięci podręcznej jest opóźnione, x86 może przywrócić rozpoczęte operacje. Ale teraz interesuje mnie tylko to, co powinienem założyć jako programista C ++, niezależnie od platformy.

[T1: wątek 1 T2: wątek 2 V1: wspólna zmienna atomowa]

Rozumiem, że std :: atomic gwarantuje, że,

(1) Żadne wyścigi danych nie występują w zmiennej (dzięki wyłącznemu dostępowi do linii pamięci podręcznej).

(2) W zależności od używanego przez nas uporządkowania pamięci, gwarantuje (z barierami), że zachodzi sekwencyjna spójność (przed barierą, po barierze lub obu).

(3) Po zapisie atomowym (V1) na T1, atomowa RMW (V1) na T2 będzie spójna (jego linia pamięci podręcznej zostanie zaktualizowana o zapisaną wartość na T1).

Ale jak wspomniałem elementarz koherencji pamięci podręcznej ,

Wszystkie te rzeczy implikują to, że domyślnie ładunki mogą pobierać nieaktualne dane (jeśli odpowiednie żądanie unieważnienia znajdowało się w kolejce unieważnień)

Czy więc poniższe informacje są prawidłowe?

(4) std::atomicNIE gwarantuje, że T2 nie odczyta „przestarzałej” wartości odczytu atomowego (V) po zapisie atomowym (V) na T1.

Pytania, czy (4) ma rację: jeśli zapis atomowy na T1 unieważnia linię pamięci podręcznej bez względu na opóźnienie, dlaczego T2 czeka na unieważnienie, gdy operacja atomowa RMW, ale nie na odczyt atomowy?

Pytania, czy (4) jest błędne: kiedy wątek może odczytać „przestarzałą” wartość i „jest widoczny” w wykonaniu, a następnie?

Bardzo doceniam twoje odpowiedzi

Aktualizacja 1

Wygląda więc na to, że się myliłem (3). Wyobraź sobie następujący przeplot dla początkowej V1 = 0:

T1: W(1)
T2:      R(0) M(++) W(1)

Chociaż w tym przypadku RMW T2 ma miejsce całkowicie po W (1), nadal może odczytać „przestarzałą” wartość (myliłem się). Zgodnie z tym atomic nie gwarantuje pełnej spójności pamięci podręcznej, tylko spójność sekwencyjną.

Aktualizacja 2

(5) Teraz wyobraź sobie ten przykład (x = y = 0 i są atomowe):

T1: x = 1;
T2: y = 1;
T3: if (x==1 && y==0) print("msg");

zgodnie z tym, o czym rozmawialiśmy, wyświetlenie „msg” wyświetlanego na ekranie nie dałoby nam informacji poza tym, że T2 zostało wykonane po T1. Mogło się zatem zdarzyć jedno z następujących wykonań:

  • T1 <T3 <T2
  • T1 <T2 <T3 (gdzie T3 widzi x = 1, ale jeszcze nie y = 1)

czy to prawda?

(6) Jeśli wątek zawsze może odczytać „przestarzałe” wartości, co by się stało, gdybyśmy przyjęli typowy scenariusz „publikowania”, ale zamiast sygnalizować, że niektóre dane są gotowe, robimy coś przeciwnego (usunąć dane)?

T1: delete gameObjectPtr; is_enabled.store(false, std::memory_order_release);
T2: while (is_enabled.load(std::memory_order_acquire)) gameObjectPtr->doSomething();

gdzie T2 nadal używałby usuniętego ptr, dopóki nie zobaczy, że is_enabled ma wartość false.

(7) Ponadto fakt, że wątki mogą odczytać „nieaktualne” wartości, oznacza, że muteksu nie można wdrożyć za pomocą tylko jednego atomowego prawa bez blokady? Wymagałoby to mechanizmu synchronizacji między wątkami. Czy wymagałby atomowej blokady?

Albert Caldas
źródło

Odpowiedzi:

3
  1. Tak, nie ma wyścigów danych
  2. Tak, przy odpowiednich memory_orderwartościach możesz zagwarantować spójność sekwencyjną
  3. Atomowy odczyt-modyfikacja-zapis zawsze będzie miał miejsce całkowicie przed lub całkowicie po zapisie atomowym do tej samej zmiennej
  4. Tak, T2 może odczytać przestarzałą wartość ze zmiennej po zapisie atomowym na T1

Atomowe operacje odczytu-modyfikacji-zapisu są określone w sposób gwarantujący ich atomowość. Jeśli inny wątek mógłby zapisać wartość po początkowym odczycie i przed zapisem operacji RMW, wówczas operacja ta nie byłaby atomowa.

Wątki zawsze mogą odczytać nieaktualne wartości, z wyjątkiem sytuacji, w których zdarza się to wcześniej, gwarantując względne porządkowanie .

Jeśli operacja RMW odczyta „przestarzałą” wartość, gwarantuje, że generowany przez nią zapis będzie widoczny przed wszelkimi zapisami z innych wątków, które zastąpiłyby odczytaną wartość.

Zaktualizuj na przykład

Jeśli T1 pisze, x=1a T2 robi x++, xpoczątkowo 0, opcje z punktu widzenia przechowywania xsą następujące:

  1. Najpierw zapis T1, więc T1 zapisuje x=1, a następnie T2 czyta x==1, zwiększa to do 2 i zapisuje x=2jako pojedynczą operację atomową.

  2. Zapis T1 jest drugi. T2 czyta x==0, zwiększa go do 1 i zapisuje x=1jako pojedynczą operację, a następnie T1 zapisuje x=1.

Jednak pod warunkiem, że nie ma innych punktów synchronizacji między tymi dwoma wątkami, wątki mogą kontynuować operacje nieprzechowane do pamięci.

W ten sposób T1 może wydać x=1, a następnie kontynuować inne czynności, nawet jeśli T2 nadal będzie czytać x==0(a więc zapisywać x=1).

Jeśli istnieją inne punkty synchronizacji, stanie się jasne, który wątek zmodyfikowano jako xpierwszy, ponieważ te punkty synchronizacji wymuszą kolejność.

Jest to najbardziej widoczne, jeśli zależy od wartości odczytanej z operacji RMW.

Aktualizacja 2

  1. Jeśli użyjesz memory_order_seq_cst(domyślnie) do wszystkich operacji atomowych, nie musisz się tym martwić. Z punktu widzenia programu, jeśli widzisz „msg”, wówczas uruchomiono T1, następnie T3, a następnie T2.

Jeśli użyjesz innych kolejności pamięci (szczególnie memory_order_relaxed), możesz zobaczyć inne scenariusze w kodzie.

  1. W takim przypadku masz błąd. Załóżmy, że is_enabledflaga jest prawdziwa, gdy T2 wejdzie w swoją whilepętlę, więc postanawia uruchomić ciało. T1 teraz usuwa dane, a następnie T2 ignoruje wskaźnik, który jest wiszącym wskaźnikiem, i pojawia się niezdefiniowane zachowanie . Atomika nie pomaga ani nie przeszkadza w żaden sposób poza zapobieganiem wyścigowi danych na fladze.

  2. Ty można wdrożyć mutex z pojedynczej zmiennej atomowej.

Anthony Williams
źródło
Wielkie dzięki @Anthony Wiliams za szybką odpowiedź. Zaktualizowałem swoje pytanie o przykład odczytu przez RMW „nieaktualnej” wartości. Patrząc na ten przykład, co rozumiesz przez względne uporządkowanie i że W (1) T2 będzie widoczne przed zapisem? Czy to oznacza, że ​​gdy T2 zobaczy zmiany T1, nie będzie już czytać W (1) T2?
Albert Caldas
Jeśli więc „Wątki zawsze mogą odczytać nieaktualne wartości”, oznacza to, że spójność pamięci podręcznej nigdy nie jest gwarantowana (przynajmniej na poziomie programisty c ++). Czy mógłbyś rzucić okiem na moją aktualizację2?
Albert Caldas
Teraz widzę, że powinienem był zwrócić większą uwagę na język i modele pamięci sprzętowej, aby w pełni zrozumieć to wszystko, czego mi brakowało. wielkie dzięki!
Albert Caldas
1

Odnośnie (3) - zależy to od zastosowanej kolejności pamięci. Jeśli wykorzystywane std::memory_order_seq_cstsą zarówno operacja przechowywania, jak i RMW , wówczas obie operacje są porządkowane w jakiś sposób - tj. Albo sklep odbywa się przed RMW, albo na odwrót. Jeśli sklep zostanie zamówiony przed RMW, wówczas jest zagwarantowane, że operacja RMW „zobaczy” zapisaną wartość. Jeśli sklep zostanie zamówiony po RMW, zastąpi on wartość zapisaną przez operację RMW.

Jeśli użyjesz bardziej złagodzonych zamówień pamięci, modyfikacje będą nadal w jakiś sposób porządkowane (kolejność modyfikacji zmiennej), ale nie masz gwarancji, czy RMW „zobaczy” wartość z operacji przechowywania - nawet jeśli operacja RMW jest kolejnością po zapisie w kolejności modyfikacji zmiennej.

Jeśli chcesz przeczytać jeszcze jeden artykuł, mogę skierować Cię do modeli pamięci dla programistów C / C ++ .

mpoeter
źródło
Dzięki za artykuł, jeszcze go nie przeczytałem. Nawet jeśli jest dość stary, przydatne było połączenie moich pomysłów.
Albert Caldas
1
Miło mi to słyszeć - ten artykuł jest nieco rozszerzonym i poprawionym rozdziałem pracy magisterskiej. :-) Koncentruje się na modelu pamięci wprowadzonym w C ++ 11; Mogę go zaktualizować, aby odzwierciedlić (małe) zmiany wprowadzone w C ++ 14/17. Daj mi znać, jeśli masz jakieś uwagi lub sugestie dotyczące ulepszeń!
mpoeter