Rozumiem, że std::atomic<>
jest to obiekt atomowy. Ale do jakiego stopnia atomowa? W moim rozumieniu operacja może być atomowa. Co dokładnie oznacza uczynienie obiektu atomowym? Na przykład, jeśli istnieją dwa wątki współbieżnie wykonujące następujący kod:
a = a + 12;
Czy zatem cała operacja (powiedzmy add_twelve_to(int)
) jest atomowa? Czy też są zmiany w zmiennej atomic (tak operator=()
)?
c++
multithreading
c++11
atomic
ciekawy facet
źródło
źródło
a.fetch_add(12)
, jak, jeśli chcesz atomowego RMW.std::atomic
pozwala bibliotece standardowej zdecydować, co jest potrzebne do osiągnięcia atomowości.std::atomic<T>
to typ, który pozwala na operacje atomowe. W magiczny sposób nie poprawia to Twojego życia, nadal musisz wiedzieć, co chcesz z tym zrobić. Jest to bardzo specyficzny przypadek użycia, a zastosowania operacji atomowych (na obiekcie) są na ogół bardzo subtelne i należy je rozpatrywać z nielokalnej perspektywy. Więc jeśli już tego nie wiesz i dlaczego chcesz operacji atomowych, typ ten prawdopodobnie nie jest dla ciebie zbyt przydatny.Odpowiedzi:
Każda instancja i pełna specjalizacja std :: atomic <> reprezentuje typ, na którym mogą jednocześnie działać różne wątki (ich instancje), bez wywoływania niezdefiniowanego zachowania:
std::atomic<>
opakowuje operacje, które w pre-C ++ 11 razy musiały być wykonywane przy użyciu (na przykład) powiązanych funkcji z MSVC lub bultinami atomowymi w przypadku GCC.Ponadto
std::atomic<>
zapewnia większą kontrolę, zezwalając na różne zamówienia pamięci, które określają synchronizację i ograniczenia kolejności. Jeśli chcesz przeczytać więcej o atomach w C ++ 11 i modelu pamięci, przydatne mogą być poniższe linki:Zauważ, że w typowych przypadkach użycia prawdopodobnie użyłbyś przeciążonych operatorów arytmetycznych lub innego ich zestawu :
Ponieważ składnia operatora nie pozwala na określenie kolejności pamięci, operacje te będą wykonywane z
std::memory_order_seq_cst
, ponieważ jest to domyślna kolejność dla wszystkich operacji atomowych w C ++ 11. Gwarantuje to sekwencyjną spójność (całkowite uporządkowanie globalne) pomiędzy wszystkimi operacjami atomowymi.W niektórych przypadkach może to jednak nie być wymagane (i nic nie jest darmowe), więc możesz chcieć użyć bardziej przejrzystego formularza:
Teraz twój przykład:
nie zostanie przeliczona na pojedynczą atomową operację: spowoduje to
a.load()
(która sama jest atomowa), a następnie dodanie między tą wartością12
aa.store()
(również atomową) wyniku końcowego. Jak wspomniałem wcześniej,std::memory_order_seq_cst
zostanie tutaj użyty.Jeśli jednak napiszesz
a += 12
, będzie to operacja atomowa (jak zauważyłem wcześniej) i jest z grubsza równoważnaa.fetch_add(12, std::memory_order_seq_cst)
.Co do twojego komentarza:
Twoje stwierdzenie jest prawdziwe tylko w przypadku architektur, które zapewniają taką gwarancję atomowości dla sklepów i / lub obciążeń. Istnieją architektury, które tego nie robią. Ponadto zwykle wymagane jest, aby operacje musiały być wykonywane na adresie wyrównanym do słów / dwordów, aby były atomowe,
std::atomic<>
co gwarantuje, że będą atomowe na każdej platformie, bez dodatkowych wymagań. Co więcej, pozwala pisać taki kod:Zwróć uwagę, że warunek asercji zawsze będzie prawdziwy (a zatem nigdy nie zostanie wyzwolony), więc zawsze możesz mieć pewność, że dane są gotowe po
while
wyjściu z pętli. Tak jest ponieważ:store()
do flagi jest wykonywane posharedData
ustawieniu (zakładamy, żegenerateData()
zawsze zwraca coś pożytecznego, w szczególności nigdy nie zwracaNULL
) i używastd::memory_order_release
order:sharedData
jest używany po zakończeniuwhile
pętli, a zatem afterload()
from flag zwróci wartość niezerową.load()
używastd::memory_order_acquire
kolejności:Daje to precyzyjną kontrolę nad synchronizacją i umożliwia jawne określenie, w jaki sposób Twój kod może / nie może / będzie / nie zachowywał się. Nie byłoby to możliwe, gdyby gwarancją była sama atomowość. Zwłaszcza jeśli chodzi o bardzo interesujące modele synchronizacji, takie jak porządkowanie wydania-konsumowania .
źródło
int
s?std::atomic
(std::memory_order
) służą dokładnie do ograniczenia dozwolonych zmian kolejności.To kwestia perspektywy ... nie można jej zastosować do dowolnych obiektów i sprawić, by ich operacje stały się atomowe, ale można użyć dostarczonych specjalizacji dla (większości) typów całkowitych i wskaźników.
std::atomic<>
nie upraszcza tego (używa wyrażeń szablonowych do) do pojedynczej operacji atomowej, zamiast tegooperator T() const volatile noexcept
element wykonuje atomowąload()
oda
, następnie dodaje się dwanaście ioperator=(T t) noexcept
robistore(t)
.źródło
int
nie zapewnia, że zmiana jest widoczna z innych wątków, ani też nie zapewnia, że zobaczysz zmiany innych wątków, a niektóre rzeczy, takie jak,my_int += 3
nie są gwarantowane atomowo, chyba że używaszstd::atomic<>
- mogą obejmować Pobierz, a następnie dodaj, a następnie zapisz sekwencję, w której inny wątek próbujący zaktualizować tę samą wartość może przyjść po pobraniu i przed zapisaniem i zepsuć aktualizację wątku.std::atomic
istnieje, ponieważ wiele ISA ma dla niego bezpośrednie wsparcie sprzętoweTo, o czym mówi standard C ++,
std::atomic
zostało przeanalizowane w innych odpowiedziach.Zobaczmy teraz, do czego się
std::atomic
kompiluje, aby uzyskać inny wgląd.Głównym wnioskiem z tego eksperymentu jest to, że współczesne procesory mają bezpośrednie wsparcie dla niepodzielnych operacji na liczbach całkowitych, na przykład przedrostek LOCK w x86, i
std::atomic
zasadniczo istnieje jako przenośny interfejs dla tych instrukcji : Co oznacza instrukcja „lock” w asemblerze x86? W wersji aarch64 zostanie użyty LDADD .To wsparcie pozwala na szybsze alternatywy dla bardziej ogólnych metod, takich jak
std::mutex
, które mogą uczynić atomowymi bardziej złożone sekcje z wieloma instrukcjami, kosztem wolniejszego niżstd::atomic
dlatego,std::mutex
że wywołujefutex
systemowe w Linuksie, który jest znacznie wolniejszy niż instrukcje środowiska użytkownika emitowane przezstd::atomic
, zobacz też: Czy std :: mutex tworzy ogrodzenie?Rozważmy następujący program wielowątkowy, który inkrementuje zmienną globalną w wielu wątkach, z różnymi mechanizmami synchronizacji w zależności od tego, który z definicji preprocesora jest używany.
main.cpp
GitHub upstream .
Kompiluj, uruchamiaj i dezasembluj:
Wyjątkowo prawdopodobne „nieprawidłowe” wyniki wyścigu dla
main_fail.out
:i deterministyczne „właściwe” wyjście innych:
Demontaż
main_fail.out
:Demontaż
main_std_atomic.out
:Demontaż
main_lock.out
:Wnioski:
wersja nieatomowa zapisuje globalny do rejestru i zwiększa rejestr.
Dlatego na końcu najprawdopodobniej cztery zapisy powrócą do globalnego z tą samą „złą” wartością
100000
.std::atomic
kompiluje się dolock addq
. Prefiks LOCK powodujeinc
niepodzielne pobieranie, modyfikowanie i aktualizowanie pamięci.nasz jawny przedrostek LOCK zestawu wbudowanego kompiluje się prawie do tego samego co
std::atomic
, z wyjątkiem tego, żeinc
zamiast niego używany jest naszadd
. Nie jestem pewien, dlaczego wybrał GCCadd
, biorąc pod uwagę, że nasz INC wygenerował dekodowanie o 1 bajt mniejsze.ARMv8 może używać LDAXR + STLXR lub LDADD w nowszych procesorach: Jak rozpocząć wątki w zwykłym C?
Przetestowano w Ubuntu 19.10 AMD64, GCC 9.2.1, Lenovo ThinkPad P51.
źródło