Jeśli istnieją dwa wątki uzyskujące dostęp do zmiennej globalnej, wiele samouczków mówi, że zmienna jest ulotna, aby zapobiec buforowaniu zmiennej przez kompilator w rejestrze, a tym samym niepoprawnej aktualizacji. Jednak dwa wątki, które uzyskują dostęp do wspólnej zmiennej, są czymś, co wymaga ochrony przez mutex, prawda? Ale w takim przypadku między zablokowaniem wątku a zwolnieniem muteksu kod znajduje się w krytycznej sekcji, w której tylko jeden wątek może uzyskać dostęp do zmiennej, w którym to przypadku zmienna nie musi być niestabilna?
Zatem jakie jest zastosowanie / cel zmienności w programie wielowątkowym?
c++
multithreading
concurrency
atomic
volatile
David Preston
źródło
źródło
Odpowiedzi:
Krótka i szybka odpowiedź :
volatile
jest (prawie) bezużyteczna w programowaniu aplikacji wielowątkowych niezależnych od platformy. Nie zapewnia synchronizacji, nie tworzy barier pamięciowych ani nie zapewnia kolejności wykonywania operacji. Nie czyni operacji atomowymi. To nie sprawia, że twój kod jest magicznie bezpieczny dla wątków.volatile
może być najbardziej niezrozumianą funkcją w całym C ++. Zobacz to , to i to, aby uzyskać więcej informacji na tematvolatile
Z drugiej strony
volatile
ma pewne zastosowanie, które może nie być tak oczywiste. Może być używany w taki sam sposób, w jaki można byconst
pomóc kompilatorowi pokazać ci, gdzie możesz popełnić błąd, uzyskując dostęp do udostępnionego zasobu w niezabezpieczony sposób. To zastosowanie zostało omówione przez Alexandrescu w tym artykule . Jest to jednak zasadniczo używanie systemu typów C ++ w sposób, który jest często postrzegany jako wymysł i może wywołać niezdefiniowane zachowanie.volatile
był specjalnie przeznaczony do użycia podczas łączenia się ze sprzętem mapowanym w pamięci, programami obsługi sygnałów i instrukcją kodu maszynowego setjmp. Ma tovolatile
bezpośrednie zastosowanie do programowania na poziomie systemu, a nie do normalnego programowania na poziomie aplikacji.Standard C ++ 2003 nie mówi, że
volatile
stosuje się do zmiennych jakikolwiek rodzaj semantyki Acquire lub Release. W rzeczywistości norma całkowicie milczy we wszystkich kwestiach wielowątkowości. Jednak określone platformy stosują semantykę Acquire and Release navolatile
zmiennych.[Aktualizacja dla C ++ 11]
Standard C ++ 11 obsługuje teraz wielowątkowość bezpośrednio w modelu pamięci i języku, a także zapewnia bibliotekę umożliwiającą radzenie sobie z nią w sposób niezależny od platformy. Jednak semantyka
volatile
nadal nie uległa zmianie.volatile
nadal nie jest mechanizmem synchronizacji. Bjarne Stroustrup mówi to samo w TCPPPL4E:[/ Zakończ aktualizację]
Dotyczy to przede wszystkim samego języka C ++, zgodnie z definicją zawartą w standardzie 2003 (a obecnie w standardzie 2011). Jednak niektóre konkretne platformy dodają dodatkowe funkcje lub ograniczenia
volatile
. Na przykład w MSVC 2010 (przynajmniej) semantyka Acquire and Release ma zastosowanie do niektórych operacji navolatile
zmiennych. Z MSDN :Możesz jednak zwrócić uwagę na fakt, że jeśli skorzystasz z powyższego łącza, w komentarzach pojawi się pewna dyskusja, czy semantyka nabywania / zwalniania faktycznie ma zastosowanie w tym przypadku.
źródło
volatile
niego, to dlatego, że stanąłeś na barkach ludzi, którzyvolatile
implementowali biblioteki wątków.volatile
tak naprawdę robi w C ++. To, co powiedział @John, jest poprawne , koniec historii. Nie ma to nic wspólnego z kodem aplikacji i kodem biblioteki, ani z „zwykłymi” kontra „boskimi wszechwiedzącymi programistami”.volatile
jest niepotrzebne i bezużyteczne do synchronizacji między wątkami. Bibliotek wątkowych nie można zaimplementować w kategoriachvolatile
; i tak musi polegać na szczegółach specyficznych dla platformy, a kiedy na nich polegasz, nie potrzebujesz jużvolatile
.(Uwaga redaktora: w C ++ 11
volatile
nie jest odpowiednim narzędziem do tego zadania i nadal ma Data-race UB. Użyjstd::atomic<bool>
zstd::memory_order_relaxed
ładowaniami / magazynami, aby to zrobić bez UB. W prawdziwych implementacjach będzie kompilować się do tego samego asm covolatile
. Dodałem odpowiedź z bardziej szczegółowo, a także rozwiązywania nieporozumień w komentarzach, że słabo uporządkowane pamięć może być problemem dla tego zastosowania literami: wszystkie procesory świata rzeczywistego mieć spójną pamięć współdzieloną takvolatile
zadziała za to na prawdziwym C ++ implementacje Ale nadal don. nie rób tego.Niektóre dyskusja w komentarzach wydaje się mówić o innych przypadkach użytkowych, gdzie będzie trzeba coś mocniejszego niż zrelaksowany atomistyki. Ta odpowiedź już wskazuje, że
volatile
nie dajesz zamówienia.)Nietrwałe jest czasami przydatne z następującego powodu: ten kod:
jest zoptymalizowany przez gcc do:
Co jest oczywiście niepoprawne, jeśli flaga jest zapisywana przez inny wątek. Zauważ, że bez tej optymalizacji mechanizm synchronizacji prawdopodobnie działa (w zależności od innego kodu mogą być potrzebne pewne bariery pamięci) - nie ma potrzeby stosowania muteksu w scenariuszu 1 producent - 1 konsument.
W przeciwnym razie słowo kluczowe volatile jest zbyt dziwne, aby było możliwe do użycia - nie zapewnia żadnego porządkowania pamięci, gwarantującego zarówno dostęp ulotny, jak i nieulotny, i nie zapewnia żadnych niepodzielnych operacji - tj. Nie otrzymujesz pomocy od kompilatora ze słowem kluczowym volatile z wyjątkiem wyłączonego buforowania rejestrów .
źródło
volatile
nie zapobiega zmianie kolejności dostępu do pamięci.volatile
dostępy nie będą zmieniane w odniesieniu do siebie nawzajem, ale nie zapewniają żadnej gwarancji zmiany kolejności w odniesieniu dovolatile
obiektów niebędących obiektami, a zatem są w zasadzie bezużyteczne również jako flagi.volatile
.while (work_left) { do_piece_of_work(); if (cancel) break;}
jeśli anulowanie jest zmieniane w pętli, logika jest nadal aktualna. Miałem fragment kodu, który działał podobnie: jeśli główny wątek chce się zakończyć, ustawia flagę dla innych wątków, ale nie ...W C ++ 11 normalnie nigdy nie używaj
volatile
do tworzenia wątków, tylko dla MMIOAle TL: DR, „działa” trochę jak atomic
mo_relaxed
na sprzęcie ze spójnymi pamięciami podręcznymi (tj. Ze wszystkim); wystarczy zatrzymać kompilatory przechowujące zmienne w rejestrach.atomic
nie potrzebuje barier pamięciowych do tworzenia atomowości lub widoczności między wątkami, tylko po to, aby bieżący wątek czekał przed / po operacji, aby utworzyć porządek między dostępami tego wątku do różnych zmiennych.mo_relaxed
nigdy nie potrzebuje żadnych barier, wystarczy załadować, przechowywać lub RMW.Dla atomów typu roll-your-own z
volatile
(i inline-asm dla barier) w starych, złych czasach przed C ++ 11std::atomic
,volatile
był to jedyny dobry sposób, aby niektóre rzeczy działały . Ale zależało to od wielu założeń dotyczących działania wdrożeń i nigdy nie było gwarantowane przez żaden standard.Na przykład jądro Linuksa nadal używa własnych, ręcznie rozwijanych atomów z rozszerzeniem
volatile
, ale obsługuje tylko kilka specyficznych implementacji C (GNU C, clang i być może ICC). Częściowo wynika to z rozszerzeń GNU C oraz składni i semantyki wbudowanego asm, ale także dlatego, że zależy to od pewnych założeń dotyczących działania kompilatorów.Prawie zawsze jest to zły wybór w przypadku nowych projektów; możesz użyć
std::atomic
(zstd::memory_order_relaxed
), aby kompilator wyemitował ten sam wydajny kod maszynowy, z którym możeszvolatile
.std::atomic
zmo_relaxed
przestarzałymivolatile
do celów gwintowania. (z wyjątkiem być może obejścia błędów po brakującej optymalizacjiatomic<double>
w niektórych kompilatorach ).Wewnętrzna implementacja
std::atomic
głównych kompilatorów (takich jak gcc i clang) nie jest wykorzystywana tylkovolatile
wewnętrznie; kompilatory bezpośrednio udostępniają funkcje atomowe load, store i RMW. (np. wbudowane GNU C,__atomic
które działają na „zwykłych” obiektach).Lotny jest użyteczny w praktyce (ale nie rób tego)
To powiedziawszy,
volatile
jest użyteczne w praktyce do takich rzeczy, jakexit_now
flaga na wszystkich (?) Istniejących implementacjach C ++ na rzeczywistych procesorach, ze względu na sposób działania procesorów (spójne pamięci podręczne) i wspólne założenia dotyczące tego, jakvolatile
powinno działać. Ale niewiele więcej i nie jest zalecane. Celem tej odpowiedzi jest wyjaśnienie, jak faktycznie działają istniejące procesory i implementacje C ++. Jeśli Cię to nie obchodzi, wszystko, co musisz wiedzieć, to to, żestd::atomic
z mo_relaxed przestarzałymi wątkamivolatile
.(Norma ISO C ++ jest dość niejasna, mówiąc tylko, że
volatile
dostęp powinien być oceniany ściśle według zasad abstrakcyjnej maszyny C ++, a nie zoptymalizowany. Biorąc pod uwagę, że rzeczywiste implementacje używają przestrzeni adresowej pamięci maszyny do modelowania przestrzeni adresowej C ++, oznacza to, żevolatile
odczyty i przypisania muszą zostać skompilowane, aby załadować / przechowywać instrukcje, aby uzyskać dostęp do reprezentacji obiektu w pamięci.)Jak wskazuje inna odpowiedź,
exit_now
flaga jest prostym przypadkiem komunikacji między wątkami, która nie wymaga żadnej synchronizacji : nie publikuje, że zawartość tablicy jest gotowa, ani nic w tym stylu. Po prostu sklep, który został natychmiast zauważony przez niezoptymalizowane ładowanie w innym wątku.Bez zmiennej lub niepodzielnej reguła as-if i założenie braku wyścigu danych UB pozwala kompilatorowi zoptymalizować go do postaci asm, która sprawdza flagę tylko raz , przed wejściem (lub nie) do nieskończonej pętli. To jest dokładnie to, co dzieje się w prawdziwym życiu dla prawdziwych kompilatorów. (I zwykle optymalizuj wiele,
do_stuff
ponieważ pętla nigdy nie kończy się, więc każdy późniejszy kod, który mógł użyć wyniku, jest nieosiągalny, jeśli wejdziemy do pętli).Program wielowątkowy, który utknął w trybie zoptymalizowanym, ale działa normalnie z -O0, jest przykładem (z opisem wyjścia asm GCC), jak dokładnie to się dzieje z GCC na x86-64. Również programowanie MCU - optymalizacja C ++ O2 przerywa pętlę na elektronice. E pokazuje inny przykład.
Zwykle chcemy agresywnych optymalizacji, które CSE i wyciągi ładują z pętli, w tym dla zmiennych globalnych.
Przed C ++ 11
volatile bool exit_now
był jeden ze sposobów, aby to działało zgodnie z przeznaczeniem (w normalnych implementacjach C ++). Ale w C ++ 11, Data-race UB nadal ma zastosowanie,volatile
więc standard ISO nie gwarantuje , że będzie działać wszędzie, nawet przy założeniu spójnych pamięci podręcznych.Należy pamiętać, że w przypadku szerszych typów
volatile
nie daje gwarancji braku łzawienia. Zignorowałem to rozróżnienie,bool
ponieważ nie jest to problem w przypadku normalnych implementacji. Ale to również część tego, dlaczegovolatile
nadal podlega UB wyścigu danych, zamiast być równoważnym zrelaksowanym atomem.Zauważ, że „zgodnie z przeznaczeniem” nie oznacza, że wątek
exit_now
oczekuje na wyjście innego wątku. Lub nawet to, że czeka, ażexit_now=true
magazyn ulotny będzie widoczny globalnie, zanim przejdzie do późniejszych operacji w tym wątku. (atomic<bool>
z domyślnym ustawieniemmo_seq_cst
będzie czekał przynajmniej przed późniejszym załadowaniem seq_cst. W wielu ISA po prostu otrzymujesz pełną barierę po sklepie).C ++ 11 zapewnia sposób inny niż UB, który kompiluje to samo
Flaga „kontynuuj działanie” lub „zakończ teraz” powinna być używana
std::atomic<bool> flag
zmo_relaxed
Za pomocą
flag.store(true, std::memory_order_relaxed)
while( !flag.load(std::memory_order_relaxed) ) { ... }
da ci dokładnie to samo asm (bez drogich instrukcji dotyczących barier), które dostałeś
volatile flag
.Oprócz braku rozrywania,
atomic
daje również możliwość przechowywania w jednym wątku i ładowania w innym bez UB, więc kompilator nie może wyciągnąć obciążenia z pętli. (Założenie o braku wyścigu danych UB jest tym, co pozwala na agresywne optymalizacje, których oczekujemy dla nieatomowych nieulotnych obiektów.) Ta cechaatomic<T>
jest prawie taka sama, jak wvolatile
przypadku czystych ładunków i czystych sklepów.atomic<T>
również zrobić+=
i tak dalej w atomowe operacje RMW (znacznie droższe niż atomowe ładowanie do tymczasowego, operacyjnego, a następnie oddzielnego atomowego magazynu. Jeśli nie chcesz atomowego RMW, napisz swój kod z lokalnym tymczasowym).Z domyślnym
seq_cst
zamówieniem, z którego otrzymałeśwhile(!flag)
, dodaje również gwarancje zamówienia wrt. dostępów nieatomowych i innych dostępów atomowych.(Teoretycznie, standard ISO C ++ nie wyklucza optymalizacji atomiki w czasie kompilacji. Jednak w praktyce kompilatory tego nie robią, ponieważ nie ma możliwości kontrolowania, kiedy to nie jest w porządku. Jest kilka przypadków, w których nawet
volatile atomic<T>
może nie być mieć wystarczającą kontrolę nad optymalizacją atomiki, jeśli kompilatory dokonały optymalizacji, więc na razie kompilatory tego nie robią. Zobacz Dlaczego kompilatory nie łączą redundantnych zapisów std :: atomic? Zauważ, że wg21 / p0062 odradza używanievolatile atomic
w bieżącym kodzie w celu ochrony przed optymalizacją atomics.)volatile
faktycznie działa w tym przypadku na prawdziwych procesorach (ale nadal go nie używa)nawet ze słabo uporządkowanymi modelami pamięci (innymi niż x86) . Ale nie używaj go, zamiast tego używaj
atomic<T>
zmo_relaxed
!! Celem tej sekcji jest odniesienie się do błędnych przekonań na temat działania rzeczywistych procesorów, a nie uzasadnienievolatile
. Jeśli piszesz kod bez zamka, prawdopodobnie zależy Ci na wydajności. Zrozumienie pamięci podręcznych i kosztów komunikacji między wątkami jest zwykle ważne dla dobrej wydajności.Prawdziwe procesory mają spójne pamięci podręczne / pamięć współdzieloną: po tym, jak magazyn z jednego rdzenia stanie się globalnie widoczny, żaden inny rdzeń nie może załadować nieaktualnej wartości. (Zobacz także Mity programistów wierzą w pamięć podręczną procesora, która mówi trochę o ulotnych składnikach Java, odpowiednik C ++
atomic<T>
z kolejnością pamięci seq_cst).Kiedy mówię load , mam na myśli instrukcję asm, która ma dostęp do pamięci. To właśnie
volatile
zapewnia dostęp i nie jest tym samym, co konwersja l-wartości do wartości r wartości nieatomowej / nieulotnej zmiennej C ++. (np.local_tmp = flag
lubwhile(!flag)
).Jedyną rzeczą, którą musisz pokonać, są optymalizacje w czasie kompilacji, które nie ładują się w ogóle po pierwszym sprawdzeniu. Każde obciążenie + sprawdzenie każdej iteracji jest wystarczające, bez żadnego zamówienia. Bez synchronizacji między tym wątkiem a głównym wątkiem nie ma sensu rozmawiać o tym, kiedy dokładnie nastąpił sklep, ani o kolejności ładowania wrt. inne operacje w pętli. Tylko wtedy, gdy jest to widoczne dla tego wątku, liczy się. Kiedy widzisz ustawioną flagę exit_now, kończysz pracę. Opóźnienie między rdzeniami w typowym Xeonie x86 może wynosić około 40 ns między oddzielnymi rdzeniami fizycznymi .
W teorii: wątki C ++ na sprzęcie bez spójnych pamięci podręcznych
Nie widzę żadnego sposobu, w jaki mogłoby to być zdalnie wydajne, z czystym ISO C ++ bez wymagania od programisty wykonywania jawnych opróżnień w kodzie źródłowym.
Teoretycznie możesz mieć implementację C ++ na maszynie, która nie jest taka, jak ta, wymagająca jawnych opróżnień generowanych przez kompilator, aby rzeczy były widoczne dla innych wątków na innych rdzeniach . (Lub do odczytu, aby nie używać być może nieaktualnej kopii). Standard C ++ nie uniemożliwia tego, ale model pamięci C ++ jest zaprojektowany tak, aby był wydajny na spójnych maszynach z pamięcią współdzieloną. Np. Standard C ++ mówi nawet o „spójności odczytu i odczytu”, „spójności zapisu i odczytu” itp. Jedna uwaga w standardzie wskazuje nawet na połączenie ze sprzętem:
Nie ma mechanizmu,
release
który pozwalałby sklepowi na opróżnianie samego siebie i kilku wybranych zakresów adresów: musiałby zsynchronizować wszystko, ponieważ nie wiedziałby, co inne wątki mogłyby chcieć przeczytać, gdyby ich pobieranie-ładowanie zobaczyło ten magazyn wydania (tworząc sekwencja wydania, która ustanawia relację wydarzyło się przed między wątkami, gwarantując, że wcześniejsze operacje nieatomowe wykonywane przez wątek piszący są teraz bezpieczne do odczytu. Chyba że dokonał dalszego zapisu do nich po magazynie wydania ...) Lub kompilatory być naprawdę sprytnym, aby udowodnić, że tylko kilka linii pamięci podręcznej wymaga opróżnienia.Powiązane: moja odpowiedź na temat Czy mov + mfence jest bezpieczne w NUMA? szczegółowo omawia nieistnienie systemów x86 bez spójnej pamięci współdzielonej. Również powiązane: Ładunki i sklepy zmieniające kolejność w ARM, aby uzyskać więcej informacji o ładunkach / sklepach do tej samej lokalizacji.
Jest to myślę, że klastry z niekoherentnego wspólna pamięć, ale nie są maszyny single-System-image. Każda domena spójności obsługuje oddzielne jądro, więc nie można w niej uruchamiać wątków pojedynczego programu C ++. Zamiast tego uruchamiasz oddzielne instancje programu (każda z własną przestrzenią adresową: wskaźniki w jednej instancji nie są prawidłowe w drugiej).
Aby zmusić ich do komunikowania się ze sobą za pomocą jawnych opróżnień, zwykle używałbyś MPI lub innego interfejsu API do przekazywania komunikatów, aby program określał, które zakresy adresów wymagają opróżnienia.
Prawdziwy sprzęt nie
std::thread
przekracza granic spójności pamięci podręcznej:Istnieją pewne asymetryczne układy ARM ze wspólną fizyczną przestrzenią adresową, ale nie z wewnętrznymi współdzielonymi domenami pamięci podręcznej. Więc nie spójne. (np. komentarz wątek rdzenia A8 i Cortex-M3 jak TI Sitara AM335x).
Ale różne jądra działałyby na tych rdzeniach, a nie pojedynczy obraz systemu, który mógłby uruchamiać wątki na obu rdzeniach. Nie znam żadnych implementacji C ++, które uruchamiają
std::thread
wątki na rdzeniach procesora bez spójnych pamięci podręcznych.W szczególności w przypadku ARM, GCC i clang generują kod przy założeniu, że wszystkie wątki działają w tej samej domenie z możliwością wewnętrznego współużytkowania. W rzeczywistości, podręcznik ARMv7 ISA mówi
Tak więc niespójna pamięć współdzielona między oddzielnymi domenami jest tylko rzeczą do jawnego, specyficznego dla systemu wykorzystania obszarów pamięci współdzielonej do komunikacji między różnymi procesami w różnych jądrach.
Zobacz także tę dyskusję CoreCLR na temat używania kodu generującego
dmb ish
(Inner Shareable Bariera) vs.dmb sy
(System) barier pamięci w tym kompilatorze.Stwierdzam, że żadna implementacja C ++ dla żadnego innego ISA nie działa
std::thread
na rdzeniach z niespójnymi pamięciami podręcznymi. Nie mam dowodu, że taka implementacja nie istnieje, ale wydaje się to wysoce nieprawdopodobne. Jeśli nie celujesz w konkretny egzotyczny element sprzętu, który działa w ten sposób, twoje myślenie o wydajności powinno zakładać spójność pamięci podręcznej między wszystkimi wątkami podobną do MESI. (Najlepiej jednak używaćatomic<T>
w sposób gwarantujący poprawność!)Spójne pamięci podręczne sprawiają, że jest to proste
Ale w systemie wielordzeniowym ze spójnymi pamięciami podręcznymi zaimplementowanie magazynu wydań oznacza po prostu zlecenie zatwierdzenia do pamięci podręcznej dla sklepów tego wątku, bez wykonywania żadnego jawnego opróżniania. ( https://preshing.com/20120913/acquire-and-release-semantics/ i https://preshing.com/20120710/memory-barriers-are-like-source-control-operations/ ). (A pobieranie oznacza zamawianie dostępu do pamięci podręcznej w drugim rdzeniu).
Instrukcja bariery pamięci po prostu blokuje ładowanie i / lub przechowywanie bieżącego wątku do momentu opróżnienia bufora magazynu; to zawsze dzieje się samoistnie tak szybko, jak to możliwe. ( Czy bariera pamięci zapewnia, że spójność pamięci podręcznej została zakończona? Rozwiązuje to błędne przekonanie). Więc jeśli nie potrzebujesz zamawiać, po prostu szybka widoczność w innych wątkach,
mo_relaxed
jest w porządku. (I tak jestvolatile
, ale nie rób tego.)Zobacz także mapowania C / C ++ 11 do procesorów
Ciekawostka: na x86 każdy magazyn asm jest magazynem wydania, ponieważ model pamięci x86 to w zasadzie seq-cst plus bufor magazynu (z przekazywaniem magazynu).
Częściowo powiązane: bufor sklepu, globalna widoczność i spójność: C ++ 11 gwarantuje bardzo niewiele. Większość prawdziwych ISA (z wyjątkiem PowerPC) gwarantuje, że wszystkie wątki mogą uzgodnić kolejność pojawiania się dwóch sklepów przez dwa inne wątki. (W formalnej terminologii związanej z modelami pamięci architektury komputerowej są one „atomami wielu kopii”).
Innym błędnym przekonaniem jest to, że instrukcje asm ogrodzenia pamięci są potrzebne do opróżnienia bufora magazynu, aby inne rdzenie mogły w ogóle zobaczyć nasze sklepy . W rzeczywistości bufor magazynu zawsze próbuje opróżnić się (zatwierdzić do pamięci podręcznej L1d) tak szybko, jak to możliwe, w przeciwnym razie zapełniłby się i wstrzymał wykonanie. To, co robi pełna bariera / ogrodzenie, zatrzymuje bieżący wątek do opróżnienia bufora sklepu , więc nasze późniejsze obciążenia pojawiają się w porządku globalnym po naszych wcześniejszych sklepach.
(Silnie uporządkowany model pamięci asm
volatile
x86 oznacza, że na x86 może skończyć się dając ci bliżejmo_acq_rel
, z wyjątkiem tego, że zmiana kolejności w czasie kompilacji ze zmiennymi nieatomowymi może nadal mieć miejsce. Ale większość modeli innych niż x86 ma słabo uporządkowane modele pamięci, więcvolatile
irelaxed
jest mniej więcej tak samo słaby, jak na tomo_relaxed
pozwala.)źródło
atomic
może prowadzić do różnych wątków mających różne wartości dla tej samej zmiennej w pamięci podręcznej . / facepalm. W pamięci podręcznej nie, w rejestrach procesora tak (ze zmiennymi nieatomowymi); Procesory używają spójnej pamięci podręcznej. Chciałbym, żeby inne pytania dotyczące SO nie były pełne wyjaśnieńatomic
rozpowszechniających się nieporozumień na temat działania procesorów. (Ponieważ jest to przydatna rzecz do zrozumienia ze względu na wydajność, a także pomaga wyjaśnić, dlaczego reguły atomowe ISO C ++ są napisane takimi, jakie są).Pewnego razu ankieter, który również uważał, że zmienność jest bezużyteczna, spierał się ze mną, że optymalizacja nie spowoduje żadnych problemów i odnosił się do różnych rdzeni mających oddzielne linie pamięci podręcznej i tak dalej (nie bardzo rozumiał, do czego dokładnie odnosi się). Ale ten fragment kodu po skompilowaniu z -O3 na g ++ (g ++ -O3 thread.cpp -lpthread) wykazuje niezdefiniowane zachowanie. Zasadniczo, jeśli wartość zostanie ustawiona przed while check, działa dobrze, a jeśli nie, przechodzi w pętlę bez zawracania sobie głowy pobieraniem wartości (która została faktycznie zmieniona przez inny wątek). Zasadniczo uważam, że wartość checkValue jest pobierana tylko raz do rejestru i nigdy nie jest ponownie sprawdzana w ramach najwyższego poziomu optymalizacji. Jeśli przed pobraniem ustawiono wartość true, działa dobrze, a jeśli nie, przechodzi w pętlę. Proszę mnie poprawić, jeśli się mylę.
źródło
volatile
? Tak, ten kod to UB - ale to też UBvolatile
.Potrzebujesz niestabilności i prawdopodobnie blokowania.
volatile mówi optymalizatorowi, że wartość może zmieniać się asynchronicznie
odczyta flagę za każdym razem w pętli.
Jeśli wyłączysz optymalizację lub sprawisz, że każda zmienna będzie ulotna, program będzie zachowywał się tak samo, ale wolniej. niestabilny oznacza po prostu „Wiem, że może właśnie to przeczytałeś i wiesz, co jest w nim napisane, ale jeśli powiem, przeczytaj to, a potem przeczytaj.
Blokowanie jest częścią programu. Nawiasem mówiąc, jeśli implementujesz semafory, to między innymi muszą one być niestabilne. (Nie próbuj tego, jest to trudne, prawdopodobnie będzie potrzebować małego asemblera lub nowego atomowego materiału, i to już zostało zrobione).
źródło
volatile
nie jest naprawdę przydatne nawet w tym przypadku. Ale zajęte czekanie jest czasami przydatną techniką.volatile
oznacza „brak zmiany kolejności”. Masz nadzieję, że oznacza to, że sklepy staną się globalnie widoczne (dla innych wątków) w kolejności programu. To właśnieatomic<T>
zmemory_order_release
lubseq_cst
daje. Ale dajevolatile
tylko gwarancję braku zmiany kolejności w czasie kompilacji : każdy dostęp pojawi się w asm w kolejności programu. Przydatne w przypadku sterownika urządzenia. I przydatne do interakcji z obsługą przerwań, debugerem lub obsługą sygnału w bieżącym rdzeniu / wątku, ale nie do interakcji z innymi rdzeniami.volatile
w praktyce wystarcza do sprawdzeniakeep_running
flagi, tak jak tutaj: Prawdziwe procesory zawsze mają spójne pamięci podręczne, które nie wymagają ręcznego opróżniania. Ale nie ma powodu, aby polecićvolatile
sięatomic<T>
zmo_relaxed
; dostaniesz to samo co m.