Dlaczego nietrwałość nie jest uważana za użyteczną w programowaniu wielowątkowym w C lub C ++?

165

Jak wykazałem w odpowiedzi, którą niedawno opublikowałem, wydaje mi się, że jestem zdezorientowany użytecznością (lub jej brakiem) volatilew kontekstach programowania wielowątkowego.

Rozumiem, że za każdym razem, gdy zmienna może zostać zmieniona poza przepływem kontroli fragmentu kodu uzyskującego do niej dostęp, ta zmienna powinna być zadeklarowana jako taka volatile. Takie sytuacje stanowią programy obsługi sygnałów, rejestry I / O i zmienne zmodyfikowane przez inny wątek.

Tak więc, jeśli masz globalną wartość int fooi foojest odczytywana przez jeden wątek i ustawiana atomowo przez inny wątek (prawdopodobnie przy użyciu odpowiedniej instrukcji maszynowej), wątek odczytujący widzi tę sytuację w ten sam sposób, w jaki widzi zmienną modyfikowaną przez program obsługi sygnału lub zmodyfikowany przez zewnętrzny stan sprzętu i dlatego foopowinien zostać zadeklarowany volatile(lub, w przypadku sytuacji wielowątkowych, dostęp z obciążeniem chronionym pamięcią, co jest prawdopodobnie lepszym rozwiązaniem).

Jak i gdzie się mylę?

Michael Ekstrand
źródło
7
Wszystko co robi volatile polega na tym, że kompilator nie powinien buforować dostępu do zmiennej lotnej. Nie mówi nic o serializacji takiego dostępu. Zostało to omówione tutaj. Nie wiem ile razy i nie sądzę, aby to pytanie dodało cokolwiek do tych dyskusji.
4
I znowu, pytanie, które na to nie zasługuje i było tu wielokrotnie zadawane, zanim zostało przyjęte. Proszę przestań to robić.
14
@neil Szukałem innych pytań i znalazłem jedno, ale jakiekolwiek istniejące wyjaśnienie, które widziałem, nie wywołało tego, czego potrzebowałem, aby naprawdę zrozumieć, dlaczego się myliłem. To pytanie wywołało taką odpowiedź.
Michael Ekstrand
1
Aby zapoznać się ze szczegółowymi badaniami na temat tego, co procesory robią z danymi (za pośrednictwem ich pamięci podręcznych), odwiedź: rdrop.com/users/paulmck/scalability/paper/whymb.2010.06.07c.pdf
Sassafras_wot
1
@curiousguy To właśnie miałem na myśli, mówiąc „nie w C”, gdzie można go używać do zapisywania w rejestrach sprzętowych itp. i nie jest używany do wielowątkowości, tak jak jest to powszechnie używane w Javie.
Monstieur

Odpowiedzi:

213

Problem volatilew kontekście wielowątkowym polega na tym, że nie zapewnia on wszystkich potrzebnych nam gwarancji. Ma kilka potrzebnych nam właściwości, ale nie wszystkie, więc nie możemy polegać volatile samodzielnie .

Jednak prymitywy, których musielibyśmy użyć dla pozostałych właściwości, zapewniają również te, które to volatilerobią, więc jest to faktycznie niepotrzebne.

Aby zapewnić bezpieczny dostęp do współdzielonych danych, potrzebujemy gwarancji, że:

  • odczyt / zapis faktycznie się dzieje (że kompilator zamiast tego po prostu przechowuje wartość w rejestrze i odkłada aktualizację pamięci głównej na dużo później)
  • że zmiana kolejności nie ma miejsca. Załóżmy, że używamy volatilezmiennej jako flagi wskazującej, czy niektóre dane są gotowe do odczytu. W naszym kodzie po prostu ustawiamy flagę po przygotowaniu danych, więc wszystko wygląda dobrze. Ale co, jeśli kolejność instrukcji zostanie zmieniona, tak aby flaga była ustawiona jako pierwsza ?

volatilegwarantuje pierwszy punkt. Gwarantuje również, że nie nastąpi zmiana kolejności między różnymi nietrwałymi odczytami / zapisami . Wszystkie volatileoperacje dostępu do pamięci będą następować w kolejności, w jakiej zostały określone. To wszystko, czego potrzebujemy do tego, co volatilejest przeznaczone: manipulowanie rejestrami we / wy lub sprzętem mapowanym w pamięci, ale nie pomaga nam to w kodzie wielowątkowym, w którym volatileobiekt jest często używany tylko do synchronizacji dostępu do danych nieulotnych. Te dostępy można nadal uporządkować w stosunku do volatiletych.

Rozwiązaniem zapobiegającym zmianie kolejności jest użycie bariery pamięci , która wskazuje zarówno kompilatorowi, jak i procesorowi, że nie można zmienić kolejności dostępu do pamięci w tym punkcie . Umieszczenie takich barier wokół naszego niestabilnego dostępu do zmiennych zapewnia, że ​​nawet nieulotne dostępy nie zostaną ponownie uporządkowane w zmiennym, umożliwiając nam pisanie kodu bezpiecznego dla wątków.

Jednak bariery pamięci zapewniają również, że wszystkie oczekujące odczyty / zapisy są wykonywane, gdy bariera zostanie osiągnięta, więc skutecznie daje nam wszystko, czego potrzebujemy, dzięki czemu staje się volatilezbędne. Możemy po prostu volatilecałkowicie usunąć kwalifikator.

Od C ++ 11 zmienne atomowe ( std::atomic<T>) dają nam wszystkie istotne gwarancje.

jalf
źródło
5
@jbcreix: O które „to” pytasz? Niestabilne czy bariery pamięci? W każdym razie odpowiedź jest prawie taka sama. Obaj muszą pracować zarówno na poziomie kompilatora, jak i procesora, ponieważ opisują obserwowalne zachowanie programu - muszą więc upewnić się, że procesor nie przestawia wszystkiego, zmieniając zachowanie, które gwarantuje. Ale obecnie nie można pisać przenośnej synchronizacji wątków, ponieważ bariery pamięci nie są częścią standardowego C ++ (więc nie są przenośne) i volatilenie są wystarczająco silne, aby były przydatne.
jalf
4
Przykład MSDN robi to i twierdzi, że nie można zmienić kolejności instrukcji poza niestabilnym dostępem: msdn.microsoft.com/en-us/library/12a04hfd(v=vs.80).aspx
OJW
27
@OJW: Ale kompilator Microsoftu na nowo definiuje volatilesię jako pełna bariera pamięci (zapobiegająca zmianie kolejności). To nie jest częścią standardu, więc nie można polegać na tym zachowaniu w kodzie przenośnym.
jalf
4
@Skizz: nie, w tym miejscu pojawia się „magia kompilatora” w równaniu. Bariera pamięci musi być zrozumiała zarówno przez procesor, jak i kompilator. Jeśli kompilator rozumie semantykę bariery pamięci, wie, aby unikać takich sztuczek (jak również zmiany kolejności odczytów / zapisów przez barierę). Na szczęście kompilator rozumie semantykę bariery pamięci, więc ostatecznie wszystko się udaje . :)
jalf
13
@Skizz: same wątki są zawsze rozszerzeniem zależnym od platformy przed C ++ 11 i C11. O ile mi wiadomo, każde środowisko C i C ++, które zapewnia rozszerzenie wątkowości, zapewnia również rozszerzenie „bariery pamięci”. Niezależnie od tego volatilejest zawsze bezużyteczna w programowaniu wielowątkowym. (Z wyjątkiem programu Visual Studio, gdzie volatile jest rozszerzeniem bariery pamięci).
Nemo
49

Możesz również rozważyć to z dokumentacji jądra Linuksa .

Programiści C często przyjmowali ulotność, co oznaczało, że zmienna może być zmieniona poza bieżącym wątkiem wykonywania; w rezultacie czasami ulegają pokusie, aby użyć go w kodzie jądra, gdy używane są współdzielone struktury danych. Innymi słowy, są znane z traktowania typów lotnych jako pewnego rodzaju łatwej zmiennej atomowej, którymi nie są. Użycie niestabilności w kodzie jądra prawie nigdy nie jest poprawne; ten dokument opisuje dlaczego.

Kluczową kwestią do zrozumienia w odniesieniu do zmienności jest to, że jej celem jest powstrzymanie optymalizacji, która prawie nigdy nie jest tym, czego naprawdę chcemy. W jądrze należy chronić współdzielone struktury danych przed niechcianym równoczesnym dostępem, co jest zupełnie innym zadaniem. Proces ochrony przed niechcianą współbieżnością pozwoli również skuteczniej uniknąć prawie wszystkich problemów związanych z optymalizacją.

Podobnie jak nietrwałe, prymitywy jądra, które zapewniają bezpieczeństwo równoczesnego dostępu do danych (blokady spinowe, muteksy, bariery pamięci itp.) Są zaprojektowane tak, aby zapobiegać niepożądanej optymalizacji. Jeśli są używane prawidłowo, nie będzie potrzeby używania również substancji lotnych. Jeśli niestabilność jest nadal konieczna, prawie na pewno gdzieś w kodzie jest błąd. W prawidłowo napisanym kodzie jądra zmienność może służyć jedynie spowolnieniu działania.

Rozważmy typowy blok kodu jądra:

spin_lock(&the_lock);
do_something_on(&shared_data);
do_something_else_with(&shared_data);
spin_unlock(&the_lock);

Jeśli cały kod jest zgodny z regułami blokowania, wartość shared_data nie może się nieoczekiwanie zmienić, gdy blokada_blokuje. Każdy inny kod, który może chcieć grać z tymi danymi, będzie czekał na zamku. Prymitywy spinlock działają jak bariery pamięci - są wyraźnie napisane, aby to robić - co oznacza, że ​​dostęp do danych nie będzie w nich optymalizowany. Zatem kompilator może pomyśleć, że wie, co będzie w shared_data, ale wywołanie spin_lock (), ponieważ działa jako bariera pamięci, zmusi go do zapomnienia wszystkiego, co wie. Nie będzie problemów z optymalizacją dostępu do tych danych.

Gdyby dane shared_data zostały zadeklarowane jako ulotne, blokowanie byłoby nadal konieczne. Ale kompilator nie byłby również w stanie zoptymalizować dostępu do shared_data w sekcji krytycznej, gdy wiemy, że nikt inny nie może z nim pracować. Gdy blokada jest utrzymywana, shared_data nie są ulotne. W przypadku współdzielonych danych właściwe blokowanie sprawia, że ​​niestabilność jest niepotrzebna - i potencjalnie szkodliwa.

Klasa pamięci ulotnej była pierwotnie przeznaczona dla rejestrów we / wy mapowanych w pamięci. Wewnątrz jądra dostęp do rejestrów również powinien być chroniony blokadami, ale nie chce się także, aby kompilator „optymalizował” dostęp do rejestrów w krytycznej sekcji. Ale w jądrze dostęp do pamięci I / O zawsze odbywa się poprzez funkcje akcesorów; dostęp do pamięci we / wy bezpośrednio przez wskaźniki jest mile widziany i nie działa na wszystkich architekturach. Te akcesory zostały napisane, aby zapobiec niechcianej optymalizacji, więc po raz kolejny niestabilność jest niepotrzebna.

Inną sytuacją, w której można by pokusić się o użycie zmiennej volatile jest sytuacja, gdy procesor jest zajęty czekaniem na wartość zmiennej. Właściwy sposób na zajęte oczekiwanie to:

while (my_variable != what_i_want)
    cpu_relax();

Wywołanie cpu_relax () może obniżyć zużycie energii przez procesor lub dać podwójnemu procesorowi z hiperwątkiem; zdarza się, że służy również jako bariera pamięci, więc po raz kolejny zmienność jest niepotrzebna. Oczywiście czekanie z zajętością jest na ogół aktem antyspołecznym.

Nadal istnieje kilka rzadkich sytuacji, w których zmienność ma sens w jądrze:

  • Wspomniane powyżej funkcje akcesorów mogą być używane w architekturach, w których bezpośredni dostęp do pamięci we / wy działa. Zasadniczo każde wywołanie akcesora staje się samo w sobie małą krytyczną sekcją i zapewnia, że ​​dostęp odbywa się zgodnie z oczekiwaniami programisty.

  • Wbudowany kod asemblera, który zmienia pamięć, ale nie ma innych widocznych skutków ubocznych, może zostać usunięty przez GCC. Dodanie słowa kluczowego volatile do instrukcji asm zapobiegnie temu usunięciu.

  • Zmienna jiffies jest wyjątkowa, ponieważ może mieć inną wartość za każdym razem, gdy jest przywoływana, ale można ją odczytać bez specjalnego blokowania. Tak więc wahania mogą być niestabilne, ale dodanie innych zmiennych tego typu jest mocno mile widziane. Pod tym względem Jiffies jest uważany za „głupią spuściznę” (słowa Linusa); naprawienie go byłoby większym kłopotem niż jest warte.

  • Wskaźniki do struktur danych w pamięci koherentnej, które mogą być modyfikowane przez urządzenia we / wy, mogą czasami być uzasadnione. Przykładem tego typu sytuacji jest bufor pierścieniowy używany przez kartę sieciową, w którym karta ta zmienia wskaźniki, aby wskazać, które deskryptory zostały przetworzone.

W przypadku większości kodów żadne z powyższych uzasadnień dla nietrwałości nie ma zastosowania. W rezultacie użycie volatile może być postrzegane jako błąd i będzie wymagało dodatkowej analizy kodu. Deweloperzy, którzy mają pokusę korzystania z niestabilności, powinni cofnąć się o krok i pomyśleć o tym, co naprawdę próbują osiągnąć.

Społeczność
źródło
3
@curiousguy: Tak. Zobacz także gcc.gnu.org/onlinedocs/gcc-4.0.4/gcc/Extended-Asm.html .
Sebastian Mach,
1
Spin_lock () wygląda jak zwykłe wywołanie funkcji. Co jest w tym szczególnego, że kompilator potraktuje go specjalnie, aby wygenerowany kod „zapomniał” o każdej wartości shared_data, która została odczytana przed spin_lock () i zapisana w rejestrze, tak aby wartość musiała zostać odczytana na nowo w do_something_on () po spin_lock ()?
Zsynchronizowano
1
@underscore_d Chodzi mi o to, że po nazwie funkcji spin_lock () nie mogę stwierdzić, że robi ona coś specjalnego. Nie wiem, co w niej jest. W szczególności nie wiem, co jest w implementacji, co uniemożliwia kompilatorowi optymalizację kolejnych odczytów.
Zsynchronizowano
1
Syncopated ma rację. Zasadniczo oznacza to, że programista powinien znać wewnętrzną implementację tych „funkcji specjalnych” lub przynajmniej być bardzo dobrze poinformowany o ich zachowaniu. Rodzi to dodatkowe pytania, takie jak - czy te specjalne funkcje są znormalizowane i gwarantowane, że będą działać w ten sam sposób na wszystkich architekturach i wszystkich kompilatorach? Czy jest dostępna lista takich funkcji lub przynajmniej istnieje konwencja używania komentarzy do kodu w celu zasygnalizowania programistom, że dana funkcja chroni kod przed „optymalizacją”?
JustAMartin
1
@Tuntable: Prywatny statyczny może zostać dotknięty przez dowolny kod, za pośrednictwem wskaźnika. A jego adres jest zajęty. Być może analiza przepływu danych jest w stanie udowodnić, że wskaźnik nigdy nie ucieka, ale jest to ogólnie bardzo trudny problem, superliniowy w rozmiarze programu. Jeśli masz sposób na zagwarantowanie, że aliasy nie istnieją, przeniesienie dostępu przez blokadę spinów powinno być w porządku. Ale jeśli nie istnieją żadne aliasy, również volatilejest bezcelowe. We wszystkich przypadkach zachowanie „wywołanie funkcji, której ciała nie można zobaczyć” będzie poprawne.
Ben Voigt,
11

Nie sądzę, że się mylisz - zmienność jest konieczna, aby zagwarantować, że wątek A zobaczy zmianę wartości, jeśli wartość zostanie zmieniona przez coś innego niż wątek A.Rozumiem, że zmienność jest w zasadzie sposobem na określenie kompilator „nie buforuj tej zmiennej w rejestrze, zamiast tego pamiętaj, aby zawsze czytać / zapisywać ją z pamięci RAM przy każdym dostępie”.

Zamieszanie polega na tym, że zmienność nie jest wystarczająca do zaimplementowania wielu rzeczy. W szczególności nowoczesne systemy używają wielu poziomów buforowania, nowoczesne wielordzeniowe procesory wykonują wymyślne optymalizacje w czasie wykonywania, a nowoczesne kompilatory wykonują wymyślne optymalizacje w czasie kompilacji, a to wszystko może powodować różne efekty uboczne pojawiające się w innym zamówienie z zamówienia, którego można by się spodziewać, gdybyś spojrzał na kod źródłowy.

Tak zmienna jest w porządku, o ile pamiętasz, że „obserwowane” zmiany zmiennej lotnej mogą nie nastąpić dokładnie w momencie, w którym myślisz, że nastąpią. W szczególności nie próbuj używać zmiennych ulotnych jako sposobu synchronizowania lub porządkowania operacji między wątkami, ponieważ nie będzie to działać niezawodnie.

Osobiście moim głównym (jedynym?) Zastosowaniem flagi volatile jest wartość logiczna „pleaseGoAwayNow”. Jeśli mam wątek roboczy, który zapętla się w sposób ciągły, każę mu sprawdzić zmienną wartość logiczną w każdej iteracji pętli i zakończyć, jeśli wartość logiczna jest kiedykolwiek prawdziwa. Wątek główny może następnie bezpiecznie wyczyścić wątek roboczy, ustawiając wartość logiczną na true, a następnie wywołując pthread_join (), aby poczekać, aż wątek roboczy zniknie.

Jeremy Friesner
źródło
2
Twoja flaga Boolean jest prawdopodobnie niebezpieczna. W jaki sposób można zagwarantować, że proces roboczy zakończy swoje zadanie i flaga pozostanie w zakresie do momentu odczytania (jeśli zostanie odczytana)? To praca na sygnały. Volatile jest dobre do implementowania prostych spinlocków, jeśli nie jest używany mutex, ponieważ bezpieczeństwo aliasów oznacza, że ​​kompilator zakłada mutex_lock(i każda inna funkcja biblioteczna) może zmienić stan zmiennej flag.
Potatoswatter
6
Oczywiście działa to tylko wtedy, gdy natura programu roboczego wątku roboczego jest taka, że ​​gwarantuje okresowe sprawdzanie wartości logicznej. Flaga volatile-bool-flag gwarantuje, że pozostanie w zakresie, ponieważ sekwencja zamykania wątków zawsze występuje przed zniszczeniem obiektu, który przechowuje wartość volatile-boolean, a sekwencja zamykania wątku wywołuje pthread_join () po ustawieniu wartości bool. pthread_join () będzie blokować do momentu zniknięcia wątku roboczego. Sygnały mają swoje własne problemy, szczególnie w połączeniu z wielowątkowością.
Jeremy Friesner
2
Wątek roboczy nie ma gwarancji, że zakończy pracę, zanim wartość logiczna stanie się prawdą - w rzeczywistości prawie na pewno będzie w środku jednostki pracy, gdy wartość logiczna jest ustawiona na true. Ale nie ma znaczenia, kiedy wątek roboczy zakończy swoją jednostkę pracy, ponieważ główny wątek nie będzie robił nic poza blokowaniem wewnątrz pthread_join (), dopóki wątek roboczy nie zostanie zamknięty. Więc sekwencja zamykania jest dobrze uporządkowana - niestabilny bool (i wszelkie inne udostępnione dane) nie zostaną zwolnione, dopóki pthread_join () nie zwróci, a pthread_join () nie powróci, dopóki wątek roboczy nie zniknie.
Jeremy Friesner
10
@Jeremy, w praktyce masz rację, ale teoretycznie może się zepsuć. W systemie dwurdzeniowym jeden rdzeń stale wykonuje wątek roboczy. Drugi rdzeń ustawia wartość bool na true. Jednak nie ma gwarancji, że rdzeń wątku roboczego kiedykolwiek zobaczy tę zmianę, tj. Może nigdy się nie zatrzymać, nawet jeśli ponownie sprawdza wartość bool. Takie zachowanie jest dozwolone przez modele pamięci c ++ 0x, java i c #. W praktyce nigdy by się to nie zdarzyło, ponieważ zajęty wątek najprawdopodobniej wstawi gdzieś barierę pamięci, po czym zobaczy zmianę na bool.
deft_code
4
Weź system POSIX, użyj polityki planowania w czasie rzeczywistym SCHED_FIFO, wyższy priorytet statyczny niż inne procesy / wątki w systemie, wystarczająca liczba rdzeni, powinna być całkowicie możliwa. W systemie Linux można określić, że proces czasu rzeczywistego może wykorzystywać 100% czasu procesora. Nigdy nie będą przełączać kontekstu, jeśli nie ma wątku / procesu o wyższym priorytecie i nigdy nie będą blokować przez operacje we / wy. Ale chodzi o to, że C / C ++ volatilenie jest przeznaczony do wymuszania prawidłowej semantyki udostępniania / synchronizacji danych. Uważam, że szukanie specjalnych przypadków, aby udowodnić, że nieprawidłowy kod może czasami zadziałać, jest bezużyteczne.
FooF
7

volatilejest przydatny (aczkolwiek niewystarczający) do implementacji podstawowej konstrukcji muteksu spinlock, ale kiedy już to masz (lub coś lepszego), nie potrzebujesz innego volatile.

Typowym sposobem programowania wielowątkowego nie jest ochrona każdej wspólnej zmiennej na poziomie maszyny, ale raczej wprowadzenie zmiennych ochronnych, które kierują przebiegiem programu. Zamiast tego volatile bool my_shared_flag;powinieneś

pthread_mutex_t flag_guard_mutex; // contains something volatile
bool my_shared_flag;

Nie tylko obejmuje to „trudną część”, ale jest z gruntu konieczne: C nie obejmuje atomowych operacji niezbędnych do zaimplementowania muteksu; musi tylko volatilezapewnić dodatkowe gwarancje dotyczące zwykłych operacji.

Teraz masz coś takiego:

pthread_mutex_lock( &flag_guard_mutex );
my_local_state = my_shared_flag; // critical section
pthread_mutex_unlock( &flag_guard_mutex );

pthread_mutex_lock( &flag_guard_mutex ); // may alter my_shared_flag
my_shared_flag = ! my_shared_flag; // critical section
pthread_mutex_unlock( &flag_guard_mutex );

my_shared_flag nie musi być niestabilny, mimo że jest nieuchronny, ponieważ

  1. Dostęp do niego ma inny wątek.
  2. Oznacza to, że odniesienie do niego musiało zostać kiedyś podjęte (z &operatorem).
    • (Lub odniesienie zostało przeniesione do struktury zawierającej)
  3. pthread_mutex_lock jest funkcją biblioteczną.
  4. Oznacza to, że kompilator nie może stwierdzić, czy w pthread_mutex_lockjakiś sposób uzyska to odniesienie.
  5. Oznacza to, że kompilator musi założyć, że pthread_mutex_lockmodyfikuje udostępnioną flagę !
  6. Dlatego zmienna musi zostać ponownie załadowana z pamięci. volatile, choć znaczący w tym kontekście, jest obcy.
Potatoswatter
źródło
6

Twoje rozumienie jest naprawdę złe.

Właściwość, jaką mają zmienne lotne, to „odczyty i zapisy do tej zmiennej są częścią dostrzegalnego zachowania programu”. Oznacza to, że ten program działa (przy odpowiednim sprzęcie):

int volatile* reg=IO_MAPPED_REGISTER_ADDRESS;
*reg=1; // turn the fuel on
*reg=2; // ignition
*reg=3; // release
int x=*reg; // fire missiles

Problem w tym, że nie jest to właściwość, której oczekujemy od czegokolwiek bezpiecznego wątkowo.

Na przykład licznik bezpieczny dla wątków byłby po prostu (kod podobny do jądra systemu Linux, nie znam odpowiednika c ++ 0x):

atomic_t counter;

...
atomic_inc(&counter);

To jest atomowe, bez bariery pamięci. W razie potrzeby należy je dodać. Dodanie volatile prawdopodobnie by nie pomogło, bo nie wiązałoby się z dostępem do pobliskiego kodu (np. Z dopisaniem elementu do listy, którą liczy licznik). Z pewnością nie musisz widzieć licznika zwiększanego poza programem, a optymalizacje są nadal pożądane, np.

atomic_inc(&counter);
atomic_inc(&counter);

nadal można zoptymalizować do

atomically {
  counter+=2;
}

czy optymalizator jest wystarczająco inteligentny (nie zmienia semantyki kodu).

jpalecek
źródło
6

Aby Twoje dane były spójne we współbieżnym środowisku, musisz spełnić dwa warunki:

1) Atomowość, tj. Jeśli czytam lub zapisuję dane w pamięci, dane te są odczytywane / zapisywane w jednym przebiegu i nie można ich przerwać ani rywalizować z powodu np. Zmiany kontekstu

2) Spójność czyli kolejność ops odczytu / zapisu musi być postrzegana być taka sama między wielu środowiskach współbieżnych - możliwe, że nici, maszyny itp

volatile nie pasuje do żadnego z powyższych - lub bardziej szczegółowo, standard c lub c ++ dotyczący tego, jak powinien zachowywać się volatile, nie obejmuje żadnego z powyższych.

W praktyce jest jeszcze gorzej, ponieważ niektóre kompilatory (takie jak kompilator Intel Itanium) próbują zaimplementować pewien element bezpiecznego zachowania współbieżnego dostępu (np. Poprzez zapewnienie barier pamięci), jednak nie ma spójności między implementacjami kompilatorów, a ponadto standard tego nie wymaga wdrożenia w pierwszej kolejności.

Oznaczanie zmiennej jako niestabilnej będzie oznaczać po prostu, że za każdym razem wymuszasz opróżnianie wartości do iz pamięci, co w wielu przypadkach po prostu spowalnia kod, ponieważ w zasadzie spadasz wydajność pamięci podręcznej.

c # i java AFAIK naprawiają to, dostosowując volatile do 1) i 2), jednak tego samego nie można powiedzieć o kompilatorach c / c ++, więc w zasadzie rób z tym, co uważasz za stosowne.

Aby uzyskać bardziej dogłębną (choć nie bezstronną) dyskusję na ten temat, przeczytaj to

zebrabox
źródło
3
+1 - gwarantowana atomowość to kolejny element tego, czego mi brakowało. Zakładałem, że ładowanie int jest atomowe, więc volatile uniemożliwiające ponowne uporządkowanie zapewnia pełne rozwiązanie po stronie odczytu. Myślę, że to przyzwoite założenie w większości architektur, ale nie jest to gwarancja.
Michael Ekstrand
Kiedy indywidualne odczyty i zapisy do pamięci są przerywane i nie atomowe? Czy jest jakaś korzyść?
batbrat
5

Comp.programming.threads FAQ ma klasyczne wyjaśnienie autorstwa Dave'a Butenhofa:

P56: Dlaczego nie muszę deklarować współdzielonych zmiennych VOLATILE?

Martwię się jednak o przypadki, w których zarówno kompilator, jak i biblioteka wątków spełniają odpowiednie specyfikacje. Zgodny kompilator C może globalnie przydzielić pewną współdzieloną (nieulotną) zmienną do rejestru, który jest zapisywany i przywracany, gdy procesor jest przekazywany z wątku do wątku. Każdy wątek będzie miał własną prywatną wartość dla tej wspólnej zmiennej, która nie jest tym, czego oczekujemy od wspólnej zmiennej.

W pewnym sensie jest to prawdą, jeśli kompilator wie wystarczająco dużo o odpowiednich zakresach zmiennej i funkcjach pthread_cond_wait (lub pthread_mutex_lock). W praktyce większość kompilatorów nie będzie próbowała przechowywać rejestrowych kopii danych globalnych przez wywołanie funkcji zewnętrznej, ponieważ zbyt trudno jest stwierdzić, czy procedura może w jakiś sposób mieć dostęp do adresu danych.

Więc tak, to prawda, że ​​kompilator, który jest ściśle zgodny (ale bardzo agresywnie) z ANSI C, może nie działać z wieloma wątkami bez ulotności. Ale niech ktoś lepiej to naprawi. Ponieważ każdy SYSTEM (to znaczy, pragmatycznie, kombinacja jądra, bibliotek i kompilatora C), który nie zapewnia gwarancji spójności pamięci POSIX, nie jest ZGODNY ze standardem POSIX. Kropka. System NIE MOŻE wymagać, abyś używał zmiennych współdzielonych w celu poprawnego zachowania, ponieważ POSIX wymaga jedynie, aby funkcje synchronizacji POSIX były konieczne.

Więc jeśli twój program się zepsuje, ponieważ nie użyłeś volatile, jest to BŁĄD. Może to nie być błąd w C, błąd w bibliotece wątków lub błąd w jądrze. Ale jest to błąd SYSTEMU i co najmniej jeden z tych składników będzie musiał pracować, aby go naprawić.

Nie chcesz używać zmiennej nieulotnej, ponieważ w każdym systemie, w którym ma to znaczenie, będzie znacznie droższa niż odpowiednia zmienna nieulotna. (ANSI C wymaga "punktów sekwencji" dla zmiennych ulotnych w każdym wyrażeniu, podczas gdy POSIX wymaga ich tylko przy operacjach synchronizacji - aplikacja wielowątkowa wymagająca dużej mocy obliczeniowej będzie widzieć znacznie więcej aktywności pamięci przy użyciu ulotnych, a przecież to aktywność pamięci naprawdę cię spowalnia.)

/ --- [Dave Butenhof] ----------------------- [[email protected]] --- \
| Digital Equipment Corporation 110 Spit Brook Rd ZKO2-3 / Q18 |
| 603.881.2218, FAKS 603.881.0120 Nashua NH 03062-2698 |
----------------- [Lepsze życie dzięki współbieżności] ---------------- /

Pan Butenhof porusza ten sam temat w tym poście w Usenecie :

Użycie „volatile” nie jest wystarczające, aby zapewnić odpowiednią widoczność pamięci lub synchronizację między wątkami. Użycie muteksu jest wystarczające i, z wyjątkiem uciekania się do różnych nieprzenośnych alternatyw kodu maszynowego (lub bardziej subtelnych implikacji reguł pamięci POSIX, które są znacznie trudniejsze do zastosowania ogólnie, jak wyjaśniono w moim poprzednim poście), mutex jest KONIECZNY.

Dlatego, jak wyjaśnił Bryan, użycie zmiennej volatile nie daje nic innego, jak tylko uniemożliwia kompilatorowi dokonywanie użytecznych i pożądanych optymalizacji, nie zapewniając żadnej pomocy w uczynieniu kodu „bezpiecznym wątkowo”. Oczywiście możesz zadeklarować wszystko, co chcesz, jako „nietrwałe” - w końcu jest to legalny atrybut pamięci masowej ANSI C. Po prostu nie oczekuj, że rozwiąże to za Ciebie jakiekolwiek problemy z synchronizacją wątków.

Wszystko to w równym stopniu dotyczy C ++.

Tony Delroy
źródło
Link jest uszkodzony; nie wydaje się już wskazywać na to, co chciałeś zacytować. Bez tekstu jest to bezsensowna odpowiedź.
jww
3

To wszystko, co robi "volatile": "Hej, kompilatorze, ta zmienna może się zmienić W DOWOLNEJ CHWILI (przy każdym tyknięciu zegara), nawet jeśli NIE działają na nią LOKALNE INSTRUKCJE. NIE buforuj tej wartości w rejestrze."

To jest to. Mówi kompilatorowi, że twoja wartość jest, no cóż, niestabilna - ta wartość może zostać zmieniona w dowolnym momencie przez zewnętrzną logikę (inny wątek, inny proces, jądro itp.). Istnieje mniej więcej wyłącznie po to, aby powstrzymać optymalizacje kompilatora, które dyskretnie buforują wartość w rejestrze, która jest z natury niebezpieczna dla EVER pamięci podręcznej.

Możesz napotkać artykuły takie jak „Dr Dobbs”, które są niestabilne jako panaceum na programowanie wielowątkowe. Jego podejście nie jest całkowicie pozbawione zalet, ale ma podstawową wadę polegającą na tym, że użytkownicy obiektu są odpowiedzialni za jego bezpieczeństwo wątków, co zwykle powoduje te same problemy, co inne naruszenia hermetyzacji.

Zack Yezek
źródło
3

Według mojego starego standardu C, „To , co stanowi dostęp do obiektu, który ma typ volatile-kwalifikowany, jest zdefiniowane przez implementację” . Tak więc autorzy kompilatorów C mogli wybrać opcję „nietrwałego”, czyli „bezpiecznego dostępu wątkowego w środowisku wieloprocesowym” . Ale nie zrobili tego.

Zamiast tego dodano operacje wymagane do zapewnienia bezpieczeństwa wątku krytycznego sekcji w wielordzeniowym środowisku pamięci współużytkowanej z wieloma procesami jako nowe funkcje zdefiniowane w ramach implementacji. Zwolnieni z wymogu, że „nietrwałość” zapewnia atomowy dostęp i porządkowanie dostępu w środowisku wieloprocesowym, autorzy kompilatorów nadali priorytet redukcji kodu w stosunku do historycznej, zależnej od implementacji, „niestabilnej” semantyki.

Oznacza to, że rzeczy takie jak „niestabilne” semafory wokół krytycznych sekcji kodu, które nie działają na nowym sprzęcie z nowymi kompilatorami, mogły kiedyś działać ze starymi kompilatorami na starym sprzęcie, a stare przykłady czasami nie są błędne, po prostu stare.

david
źródło
Stare przykłady wymagały, aby program był przetwarzany przez wysokiej jakości kompilatory, które są odpowiednie do programowania niskopoziomowego. Niestety, "nowoczesne" kompilatory przyjęły fakt, że Standard nie wymaga od nich przetwarzania "ulotnych" w użyteczny sposób, jako wskazówkę, że kod, który by tego wymagał, jest uszkodzony, zamiast uznawać, że Standard nie czyni próba zakazania implementacji, które są zgodne, ale tak niskiej jakości, że są bezużyteczne, ale w żaden sposób nie akceptują kompilatorów niskiej jakości, ale zgodnych, które stały się popularne
supercat
Na większości platform dość łatwo byłoby rozpoznać, co volatilenależy zrobić, aby umożliwić napisanie systemu operacyjnego w sposób zależny od sprzętu, ale niezależny od kompilatora. Wymaganie, aby programiści używali funkcji zależnych od implementacji zamiast wykonywania volatilepracy zgodnie z wymaganiami, podważa cel posiadania standardu.
supercat