Jest to po prostu sposób, w jaki zmienne warunkowe są (lub były pierwotnie) implementowane.
Muteks służy do ochrony samej zmiennej warunkowej . Dlatego musisz go zablokować, zanim zaczniesz czekać.
Oczekiwanie „atomowo” odblokuje muteks, umożliwiając innym dostęp do zmiennej warunkowej (do sygnalizacji). Następnie, gdy zmienna warunku zostanie zasygnalizowana lub nadana, jeden lub więcej wątków na liście oczekujących zostanie obudzonych, a muteks zostanie ponownie magicznie zablokowany dla tego wątku.
Zazwyczaj widzisz następującą operację ze zmiennymi warunku, ilustrującymi ich działanie. Poniższy przykład to wątek roboczy, który otrzymuje pracę za pośrednictwem sygnału do zmiennej warunkowej.
thread:
initialise.
lock mutex.
while thread not told to stop working:
wait on condvar using mutex.
if work is available to be done:
do the work.
unlock mutex.
clean up.
exit thread.
Praca jest wykonywana w ramach tej pętli, pod warunkiem, że będzie dostępna po powrocie oczekiwania. Gdy wątek zostanie oflagowany, aby przestał działać (zwykle przez inny wątek ustawiający warunek wyjścia, a następnie uruchamiający zmienną warunkową, aby obudzić ten wątek), pętla zostanie zamknięta, muteks zostanie odblokowany i ten wątek zostanie zamknięty.
Powyższy kod jest modelem dla jednego konsumenta, ponieważ muteks pozostaje zablokowany podczas pracy. W przypadku odmiany dla wielu konsumentów możesz użyć jako przykładu :
thread:
initialise.
lock mutex.
while thread not told to stop working:
wait on condvar using mutex.
if work is available to be done:
copy work to thread local storage.
unlock mutex.
do the work.
lock mutex.
unlock mutex.
clean up.
exit thread.
co pozwala innym konsumentom otrzymywać pracę, podczas gdy ten wykonuje pracę.
Zmienna warunkowa odciąża cię od odpytywania jakiegoś warunku, a zamiast tego pozwala innemu wątkowi powiadomić cię, gdy coś się wydarzy. Inny wątek może powiedzieć, że ten wątek jest dostępny, jak następuje:
lock mutex.
flag work as available.
signal condition variable.
unlock mutex.
Ogromna większość często błędnie nazywanych fałszywymi pobudkami była generalnie zawsze dlatego, że w ich pthread_cond_wait
wywołaniu (transmisji) zasygnalizowano wiele wątków , jeden wrócił z muteksem, wykonał pracę, a następnie czekał ponownie.
Wtedy drugi sygnalizowany wątek mógłby wyjść, gdy nie było żadnej pracy do wykonania. Trzeba było więc mieć dodatkową zmienną wskazującą, że należy wykonać pracę (była z natury chroniona mutex za pomocą pary condvar / mutex tutaj - inne wątki musiały zablokować muteks przed jego zmianą).
To było technicznie możliwe do nitki, aby powrócić ze stanu oczekiwania bez wyrzucony przez inny proces (jest to prawdziwe fałszywe pobudki), ale we wszystkich moich wielu lat pracuje nad pthreads, zarówno w rozwoju / usługa kodu i jako użytkownik z nich nigdy nie otrzymałem żadnego z nich. Może to tylko dlatego, że HP miał przyzwoitą implementację :-)
W każdym razie ten sam kod, który obsługiwał błędną skrzynkę, również obsługiwał autentyczne fałszywe pobudki, ponieważ flaga dostępności pracy nie byłaby dla nich ustawiona.
do something
wwhile
pętli?Zmienna warunku jest dość ograniczona, jeśli można tylko zasygnalizować warunek, zwykle trzeba obsłużyć niektóre dane związane z sygnalizowanym warunkiem. Sygnalizacja / pobudka muszą być wykonywane atomowo, aby osiągnąć to bez wprowadzania warunków wyścigu lub być zbyt skomplikowane
pthreads może również dać ci, z raczej technicznych powodów, fałszywe przebudzenie . Oznacza to, że musisz sprawdzić predykat, aby mieć pewność, że warunek został rzeczywiście zasygnalizowany - i odróżnić go od fałszywego budzenia. Sprawdzanie takiego stanu pod kątem oczekiwania na jego ochronę musi być strzeżone - więc zmienna warunku potrzebuje sposobu na atomowe oczekiwanie / budzenie podczas blokowania / odblokowywania muteksu strzegącego tego warunku.
Rozważ prosty przykład, w którym otrzymasz powiadomienie, że niektóre dane są generowane. Być może inny wątek utworzył dane, które chcesz, i ustawił wskaźnik na te dane.
Wyobraź sobie, że wątek producenta przekazuje dane do innego wątku konsumenta za pomocą wskaźnika „some_data”.
naturalnie miałbyś dużo warunków wyścigowych, co by było, gdyby drugi wątek zrobił się
some_data = new_data
zaraz po przebudzeniu, ale zanim to zrobiłeśdata = some_data
Nie możesz tak naprawdę stworzyć własnego muteksu, aby strzec tej sprawy .eg
Nie zadziała, wciąż istnieje szansa na wyścig między przebudzeniem a złapaniem muteksu. Umieszczenie muteksu przed pthread_cond_wait nie pomaga, ponieważ teraz będziesz trzymać muteks podczas oczekiwania - tj. Producent nigdy nie będzie w stanie złapać muteksu. (zwróć uwagę, że w tym przypadku możesz utworzyć drugą zmienną warunkową, aby zasygnalizować producentowi, że skończyłeś
some_data
- choć stanie się to skomplikowane, szczególnie jeśli chcesz wielu producentów / konsumentów).Dlatego potrzebujesz sposobu na atomowe zwolnienie / złapanie muteksu podczas oczekiwania / budzenia się z tego stanu. To właśnie robią zmienne warunkowe pthread, a oto co byś zrobił:
(producent musiałby oczywiście podjąć takie same środki ostrożności, zawsze strzegąc „some_data” za pomocą tego samego muteksu i upewniając się, że nie nadpisze some_data, jeśli some_data jest obecnie! = NULL)
źródło
while (some_data != NULL)
być pętlą „do-while”, aby przynajmniej raz czekała na zmienną warunku?while(some_data != NULL)
byćwhile(some_data == NULL)
?Zmienne warunkowe POSIX są bezstanowe. Twoim obowiązkiem jest utrzymanie stanu. Ponieważ stan będzie dostępny zarówno dla wątków oczekujących, jak i wątków, które każą innym wątkom przestać czekać, musi być chroniony przez muteks. Jeśli uważasz, że możesz używać zmiennych warunkowych bez muteksu, to nie zrozumiałeś, że zmienne warunkowe są bezstanowe.
Zmienne warunkowe są zbudowane wokół warunku. Wątki oczekujące na zmienną warunku oczekują na pewien warunek. Wątki sygnalizujące zmienne warunku zmieniają ten warunek. Na przykład wątek może oczekiwać na dostarczenie niektórych danych. Inny wątek może zauważyć, że dane dotarły. „Dane dotarły” jest warunkiem.
Oto klasyczne zastosowanie uproszczonej zmiennej warunkowej:
Zobacz, jak wątek czeka na pracę. Praca jest chroniona przez mutex. Oczekiwanie zwalnia muteks, aby inny wątek mógł dać temu wątkowi trochę pracy. Oto jak zostanie to zasygnalizowane:
Zauważ, że potrzebujesz muteksu do ochrony kolejki roboczej. Zauważ, że sama zmienna warunku nie ma pojęcia, czy jest praca, czy nie. Oznacza to, że zmienna warunku musi być powiązana z warunkiem, warunek ten musi być utrzymywany przez twój kod, a ponieważ jest dzielony między wątkami, musi być chroniony przez muteks.
źródło
Nie wszystkie funkcje zmiennych warunkowych wymagają muteksu: robią to tylko operacje oczekiwania. Sygnały i operacje rozgłoszeniowe nie wymagają muteksu. Zmienna warunkowa również nie jest trwale powiązana z konkretnym muteksem; zewnętrzny muteks nie chroni zmiennej warunkowej. Jeśli zmienna warunkowa ma stan wewnętrzny, taki jak kolejka oczekujących wątków, musi być chroniona przez wewnętrzną blokadę wewnątrz zmiennej warunkowej.
Operacje oczekiwania łączą zmienną warunkową i muteks, ponieważ:
Z tego powodu operacja oczekiwania przyjmuje jako argumenty zarówno muteks, jak i warunek: tak, aby mógł zarządzać przeniesieniem atomowym wątku z posiadania muteksu do oczekiwania, tak aby wątek nie padł ofiarą przegranego wyścigu pobudki .
Utrata wyścigu po przebudzeniu nastąpi, jeśli wątek porzuci muteks, a następnie czeka na bezstanowy obiekt synchronizacji, ale w sposób, który nie jest atomowy: istnieje okno czasowe, w którym wątek nie ma już blokady i ma jeszcze nie zaczął czekać na obiekt. W tym oknie może wejść inny wątek, sprawić, że oczekiwany warunek będzie prawdziwy, zasygnalizować bezstanową synchronizację, a następnie zniknąć. Obiekt bezstanowy nie pamięta, że został zasygnalizowany (jest bezstanowy). Tak więc oryginalny wątek zasypia na bezstanowym obiekcie synchronizacji i nie budzi się, mimo że warunek, którego potrzebuje, już się spełnił: utracone budzenie.
Funkcje czekania zmiennej warunkowej unikają utraconego budzenia przez upewnienie się, że wątek wywołujący jest zarejestrowany, aby niezawodnie wychwycić budzenie, zanim zrezygnuje z muteksu. Byłoby to niemożliwe, gdyby funkcja oczekiwania zmiennej warunkowej nie przyjęła muteksu jako argumentu.
źródło
pthread_cond_broadcast
ipthread_cond_signal
operacje (których dotyczy to pytanie SO) nawet nie traktują muteksu jako argumentu; tylko warunek. Specyfikacja POSIX jest tutaj . Muteks jest wspomniany tylko w odniesieniu do tego, co dzieje się w oczekujących wątkach, gdy się budzą.Nie uważam, aby inne odpowiedzi były tak zwięzłe i czytelne jak ta strona . Normalnie kod oczekiwania wygląda mniej więcej tak:
Istnieją trzy powody, aby zawrzeć
wait()
mutex:signal()
przedtemwait()
i tęsknilibyśmy za tym przebudzeniem.check()
zależy od modyfikacji z innego wątku, więc i tak potrzebujesz wzajemnego wykluczenia.Trzeci punkt nie zawsze jest problemem - kontekst historyczny jest powiązany z artykułem z tą rozmową .
O tym mechanizmie często wspominane są fałszywe pobudki (tzn. Oczekujący wątek jest budzony bez
signal()
wywoływania). Jednak takie zdarzenia są obsługiwane przez zapętlonecheck()
.źródło
Zmienne warunkowe są powiązane z muteksem, ponieważ jest to jedyny sposób, w jaki można uniknąć rasy, której ma unikać.
W tym momencie nie ma wątku, który zasygnalizuje zmienną warunkową, więc wątek1 będzie czekać wiecznie, mimo że chronionaReadyToRunVariable mówi, że jest gotowy do pracy!
Jedynym sposobem na obejście tego jest to, że zmienne warunkowe atomowo zwalniają muteks, jednocześnie zaczynając czekać na zmienną warunkową. Dlatego funkcja cond_wait wymaga muteksu
źródło
Muteks powinien być zablokowany podczas połączenia
pthread_cond_wait
; kiedy go nazwiesz, atomowo odblokowuje muteks, a następnie blokuje warunek. Po zasygnalizowaniu warunku atomowo blokuje go ponownie i wraca.Pozwala to na implementację przewidywalnego szeregowania, jeśli jest to pożądane, w tym sensie, że wątek, który wykonuje sygnalizację, może poczekać, aż muteks zostanie zwolniony, aby wykonać przetwarzanie, a następnie zasygnalizować warunek.
źródło
Zrobiłem ćwiczenie w klasie, jeśli chcesz prawdziwy przykład zmiennej warunkowej:
źródło
Wydaje się, że jest to konkretna decyzja projektowa, a nie potrzeba koncepcyjna.
Według dokumentów pthreads powodem, dla którego muteks nie został rozdzielony, jest znaczna poprawa wydajności, łącząc je i oczekują, że z powodu typowych warunków rasowych, jeśli nie użyjesz muteksu, prawie zawsze tak będzie.
https://linux.die.net/man/3/pthread_cond_wait
źródło
Istnieje mnóstwo egzegezy na ten temat, ale chcę to uosabiać za pomocą następującego przykładu.
Co jest nie tak z fragmentem kodu? Zastanów się trochę, zanim przejdziesz dalej.
Sprawa jest naprawdę subtelna. Jeśli rodzic przywoła,
thr_parent()
a następnie zweryfikuje wartośćdone
, zobaczy, że tak,0
i spróbuje iść spać. Ale tuż przed wezwaniem do przejścia w tryb uśpienia rodzic zostaje przerwany między wierszami 6-7, a dziecko biegnie. Dziecko zmienia zmienną stanudone
na1
i sygnalizuje, ale żaden wątek nie czeka i dlatego wątek nie jest budzony. Kiedy rodzic biegnie ponownie, śpi wiecznie, co jest naprawdę rażące.Co się stanie, jeśli zostaną przeprowadzone indywidualnie, gdy nabyte zamki?
źródło