Jak wykazałem w odpowiedzi, którą niedawno opublikowałem, wydaje mi się, że jestem zdezorientowany użytecznością (lub jej brakiem) volatile
w 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 foo
i foo
jest 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 foo
powinien 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ę?
Odpowiedzi:
Problem
volatile
w 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
volatile
robią, więc jest to faktycznie niepotrzebne.Aby zapewnić bezpieczny dostęp do współdzielonych danych, potrzebujemy gwarancji, że:
volatile
zmiennej 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 ?volatile
gwarantuje pierwszy punkt. Gwarantuje również, że nie nastąpi zmiana kolejności między różnymi nietrwałymi odczytami / zapisami . Wszystkievolatile
operacje dostępu do pamięci będą następować w kolejności, w jakiej zostały określone. To wszystko, czego potrzebujemy do tego, covolatile
jest przeznaczone: manipulowanie rejestrami we / wy lub sprzętem mapowanym w pamięci, ale nie pomaga nam to w kodzie wielowątkowym, w którymvolatile
obiekt jest często używany tylko do synchronizacji dostępu do danych nieulotnych. Te dostępy można nadal uporządkować w stosunku dovolatile
tych.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ę
volatile
zbędne. Możemy po prostuvolatile
całkowicie usunąć kwalifikator.Od C ++ 11 zmienne atomowe (
std::atomic<T>
) dają nam wszystkie istotne gwarancje.źródło
volatile
nie są wystarczająco silne, aby były przydatne.volatile
się 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.volatile
jest zawsze bezużyteczna w programowaniu wielowątkowym. (Z wyjątkiem programu Visual Studio, gdzie volatile jest rozszerzeniem bariery pamięci).Możesz również rozważyć to z dokumentacji jądra Linuksa .
źródło
volatile
jest bezcelowe. We wszystkich przypadkach zachowanie „wywołanie funkcji, której ciała nie można zobaczyć” będzie poprawne.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.
źródło
mutex_lock
(i każda inna funkcja biblioteczna) może zmienić stan zmiennej flag.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 ++volatile
nie 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.volatile
jest przydatny (aczkolwiek niewystarczający) do implementacji podstawowej konstrukcji muteksu spinlock, ale kiedy już to masz (lub coś lepszego), nie potrzebujesz innegovolatile
.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śNie tylko obejmuje to „trudną część”, ale jest z gruntu konieczne: C nie obejmuje atomowych operacji niezbędnych do zaimplementowania muteksu; musi tylko
volatile
zapewnić dodatkowe gwarancje dotyczące zwykłych operacji.Teraz masz coś takiego:
my_shared_flag
nie musi być niestabilny, mimo że jest nieuchronny, ponieważ&
operatorem).pthread_mutex_lock
jest funkcją biblioteczną.pthread_mutex_lock
jakiś sposób uzyska to odniesienie.pthread_mutex_lock
modyfikuje udostępnioną flagę !volatile
, choć znaczący w tym kontekście, jest obcy.źródło
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):
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):
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.
nadal można zoptymalizować do
czy optymalizator jest wystarczająco inteligentny (nie zmienia semantyki kodu).
źródło
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
źródło
Comp.programming.threads FAQ ma klasyczne wyjaśnienie autorstwa Dave'a Butenhofa:
Pan Butenhof porusza ten sam temat w tym poście w Usenecie :
Wszystko to w równym stopniu dotyczy C ++.
źródło
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.
źródło
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.
źródło
volatile
należ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 wykonywaniavolatile
pracy zgodnie z wymaganiami, podważa cel posiadania standardu.