Nie musisz trzymać blokady podczas dzwonienia condition_variable::notify_one()
, ale nie jest to złe w tym sensie, że jest to nadal dobrze zdefiniowane zachowanie, a nie błąd.
Jednak może to być „pesymizacja”, ponieważ każdy oczekujący wątek, który zostanie uruchomiony (jeśli istnieje), natychmiast spróbuje uzyskać blokadę utrzymywaną przez wątek powiadamiający. Myślę, że dobrą zasadą jest unikanie trzymania blokady związanej ze zmienną warunku podczas wywoływania notify_one()
lub notify_all()
. Zobacz Pthread Mutex: pthread_mutex_unlock () zajmuje dużo czasu na przykład, w którym zwolnienie blokady przed wywołaniem odpowiednika pthread notify_one()
poprawiającej wydajność w wymierny sposób.
Należy pamiętać, że lock()
wywołanie w while
pętli jest w pewnym momencie konieczne, ponieważ blokada musi być utrzymywana podczas while (!done)
sprawdzania stanu pętli. Ale nie trzeba go wstrzymywać, aby zadzwonić notify_one()
.
2016-02-27 : Duża aktualizacja w celu rozwiązania niektórych pytań w komentarzach dotyczących tego, czy istnieje sytuacja wyścigu, czy blokada, nie pomaga w przypadku notify_one()
połączenia. Wiem, że ta aktualizacja jest spóźniona, ponieważ pytanie zostało zadane prawie dwa lata temu, ale chciałbym odpowiedzieć na pytanie @ Cookie dotyczące możliwego stanu wyścigu, jeśli producent ( signals()
w tym przykładzie) dzwoni notify_one()
tuż przed konsumentem ( waits()
w tym przykładzie) w stanie zadzwonić wait()
.
Kluczem jest to, co się dzieje i
- to obiekt, który faktycznie wskazuje, czy konsument ma „pracę” do wykonania. To condition_variable
tylko mechanizm pozwalający konsumentowi efektywnie czekać na zmianę i
.
Producent musi trzymać zamek podczas aktualizacji i
, a konsument musi przytrzymać zamek podczas sprawdzania i
i dzwonienia condition_variable::wait()
(jeśli w ogóle musi czekać). W tym przypadku kluczem jest to, że musi to być ten sam przypadek przytrzymania zamka (często nazywanego sekcją krytyczną), gdy konsument wykonuje to sprawdzanie i czekanie. Ponieważ sekcja krytyczna jest utrzymywana, gdy producent aktualizuje i
i gdy konsument sprawdza i czeka i
, nie ma możliwości i
zmiany między momentem sprawdzenia i
a wezwaniem condition_variable::wait()
. To jest sedno prawidłowego użycia zmiennych warunkowych.
Standard C ++ mówi, że zmienna condition_variable :: wait () zachowuje się jak poniższa, gdy jest wywoływana z predykatem (jak w tym przypadku):
while (!pred())
wait(lock);
Podczas sprawdzania konsumenta mogą wystąpić dwie sytuacje i
:
jeśli i
wynosi 0, to klient dzwoni cv.wait()
, to i
nadal będzie 0, gdy wait(lock)
wywoływana jest część implementacji - zapewnia to właściwe użycie blokad. W tym przypadku producent nie ma możliwości, aby połączyć się condition_variable::notify_one()
w while
pętli, dopóki konsument nazwał cv.wait(lk, []{return i == 1;})
(a wait()
rozmowa zrobiła wszystko, co trzeba zrobić, aby prawidłowo „catch” a zawiadomić - wait()
nie będzie zwolnić blokadę, dopóki nie zrobisz ). W tym przypadku konsument nie może przegapić powiadomienia.
jeśli w i
momencie wywołania konsumenta jest już 1 cv.wait()
, wait(lock)
część implementacji nigdy nie zostanie wywołana, ponieważ while (!pred())
test spowoduje zakończenie pętli wewnętrznej. W tej sytuacji nie ma znaczenia, kiedy nastąpi wywołanie notify_one () - konsument nie zablokuje się.
Przykład tutaj ma dodatkową złożoność polegającą na używaniu done
zmiennej do sygnalizowania z powrotem wątkowi producenta, że konsument to rozpoznał i == 1
, ale nie sądzę, aby to w ogóle zmieniło analizę, ponieważ cały dostęp do done
(zarówno do odczytu, jak i modyfikacji ) są wykonywane w tych samych krytycznych sekcjach, które obejmują i
i condition_variable
.
Jeśli spojrzeć na pytanie, które @ EH9 wskazał, Sync jest zawodna przy użyciu std :: atomowej i std :: condition_variable , to zostanie wyświetlony stan wyścigu. Jednak kod zamieszczony w tym pytaniu narusza jedną z fundamentalnych zasad używania zmiennej warunkowej: nie zawiera ani jednej krytycznej sekcji podczas wykonywania sprawdzenia i czekania.
W tym przykładzie kod wygląda następująco:
if (--f->counter == 0)
f->resume.notify_all();
else
{
unique_lock<mutex> lock(f->resume_mutex);
f->resume.wait(lock);
}
Zauważysz, że wait()
at # 3 jest wykonywane podczas trzymania f->resume_mutex
. Ale sprawdzenie, czy wait()
w kroku 1 jest konieczne, nie jest wykonywane przy utrzymywaniu tej blokady w ogóle (znacznie mniej w sposób ciągły w przypadku sprawdzania i czekania), co jest wymagane do prawidłowego użycia zmiennych warunkowych). Uważam, że osoba, która ma problem z tym fragmentem kodu, pomyślała, że ponieważ f->counter
był to std::atomic
typ, spełniłby to wymaganie. Jednak atomowość zapewniana przez std::atomic
nie obejmuje kolejnego wywołania f->resume.wait(lock)
. W tym przykładzie istnieje wyścig między momentem f->counter
zaznaczenia (krok 1) a wait()
wywołaniem (krok 3).
Ta rasa nie istnieje w przykładzie tego pytania.
wait morphing
optymalizację) Praktyczna zasada wyjaśniona w tym linku: powiadamiaj blokadę Z jest lepsze w sytuacjach z więcej niż 2 wątkami, aby uzyskać bardziej przewidywalne wyniki.the_condition_variable.wait(lock);
. Jeśli nie ma blokady potrzebnej do zsynchronizowania producenta i konsumenta (powiedzmy, że podstawą jest kolejka spsc wolna od blokad), to ta blokada nie ma sensu, jeśli producent jej nie zablokuje. Dla mnie nie ma problemu. Ale czy nie ma ryzyka rzadkiej rasy? Jeśli producent nie blokuje zamka, czy nie mógłby zadzwonić notify_one, gdy konsument jest tuż przed czekaniem? Wtedy konsument czeka i nie budzi się ...std::cout << "Waiting... \n";
gdy producent to robicv.notify_one();
, wtedy znika sygnał budzenia ... A może coś mi tu brakuje?Sytuacja
Używając vc10 i Boost 1.56, zaimplementowałem kolejkę współbieżną, tak jak sugeruje ten post na blogu . Autor odblokowuje muteks, aby zminimalizować rywalizację, tj.
notify_one()
Jest wywoływany z odblokowanym muteksem:void push(const T& item) { std::unique_lock<std::mutex> mlock(mutex_); queue_.push(item); mlock.unlock(); // unlock before notificiation to minimize mutex contention cond_.notify_one(); // notify one waiting thread }
Odblokowanie muteksu jest poparte przykładem w dokumentacji Boost :
void prepare_data_for_processing() { retrieve_data(); prepare_data(); { boost::lock_guard<boost::mutex> lock(mut); data_ready=true; } cond.notify_one(); }
Problem
Mimo to doprowadziło to do następującego błędnego zachowania:
notify_one()
jeszcze nie został wywołany,cond_.wait()
nadal można go przerwać za pomocąboost::thread::interrupt()
notify_one()
został wezwany po raz pierwszy docond_.wait()
impasu; czekania nie można już zakończyćboost::thread::interrupt()
aniboost::condition_variable::notify_*()
już dłużej.Rozwiązanie
Usunięcie linii
mlock.unlock()
sprawiło, że kod działał zgodnie z oczekiwaniami (powiadomienia i przerwania kończą oczekiwanie). Zauważ, żenotify_one()
wywoływane jest, gdy muteks jest nadal zablokowany, jest odblokowywany zaraz po opuszczeniu lunety:void push(const T& item) { std::lock_guard<std::mutex> mlock(mutex_); queue_.push(item); cond_.notify_one(); // notify one waiting thread }
Oznacza to, że przynajmniej w przypadku mojej konkretnej implementacji wątku mutex nie może zostać odblokowany przed wywołaniem
boost::condition_variable::notify_one()
, chociaż oba sposoby wydają się prawidłowe.źródło
Jak zauważyli inni, nie musisz trzymać blokady podczas dzwonienia
notify_one()
, jeśli chodzi o warunki wyścigu i problemy związane z wątkami. Jednak w niektórych przypadkach może być wymagane przytrzymanie blokady, aby zapobieccondition_variable
zniszczeniu przednotify_one()
wywołaniem. Rozważmy następujący przykład:thread t; void foo() { std::mutex m; std::condition_variable cv; bool done = false; t = std::thread([&]() { { std::lock_guard<std::mutex> l(m); // (1) done = true; // (2) } // (3) cv.notify_one(); // (4) }); // (5) std::unique_lock<std::mutex> lock(m); // (6) cv.wait(lock, [&done]() { return done; }); // (7) } void main() { foo(); // (8) t.join(); // (9) }
Załóżmy, że istnieje przełączenie kontekstu do nowo utworzonego wątku
t
po jego utworzeniu, ale zanim zaczniemy czekać na zmienną warunku (gdzieś pomiędzy (5) a (6)). Wątekt
uzyskuje blokadę (1), ustawia zmienną predykatu (2), a następnie zwalnia blokadę (3). Załóżmy, że w tym momencie istnieje inny przełącznik kontekstu przed wykonaniemnotify_one()
(4). Wątek główny przejmuje blokadę (6) i wykonuje wiersz (7), w którym to momencie predykat powracatrue
i nie ma powodu czekać, więc zwalnia blokadę i kontynuuje.foo
zwraca (8) i zmienne w swoim zakresie (w tym (4), w którym to momencie jest już zniszczone!cv
) są niszczone. Zanim wątekt
mógł dołączyć do głównego wątku (9), musi zakończyć wykonywanie, więc kontynuuje od miejsca, w którym został przerwanycv.notify_one()
cv
Możliwym rozwiązaniem w tym przypadku jest trzymanie blokady podczas wywoływania
notify_one
(tj. Usunięcie zakresu kończącego się w linii (3)). W ten sposób zapewniamy, żet
wywołania wątkunotify_one
wcześniej będącv.wait
mogły sprawdzić nowo ustawioną zmienną predykatu i kontynuować, ponieważ wt
celu wykonania sprawdzenia musiałaby uzyskać blokadę, która jest obecnie utrzymywana. Tak więc zapewniamy, że po powrociecv
nie ma do niego dostępu wątek .t
foo
Podsumowując, problem w tym konkretnym przypadku nie dotyczy tak naprawdę wątków, ale czasów życia zmiennych przechwyconych przez odniesienie.
cv
jest przechwytywany przez odniesienie za pośrednictwem wątkut
, dlatego musisz upewnić się, żecv
pozostaje żywy przez czas wykonywania wątku. Inne przedstawione tutaj przykłady nie cierpią z powodu tego problemu, ponieważ obiektycondition_variable
imutex
są zdefiniowane w zasięgu globalnym, dlatego gwarantuje się, że pozostaną one żywe do momentu zakończenia programu.źródło
@Michael Burr ma rację.
condition_variable::notify_one
nie wymaga blokady zmiennej. Nic nie stoi jednak na przeszkodzie, aby w takiej sytuacji użyć zamka, co ilustruje przykład.W podanym przykładzie blokada jest motywowana równoczesnym użyciem zmiennej
i
. Ponieważsignals
wątek modyfikuje zmienną, musi zapewnić, że żaden inny wątek nie będzie miał do niej dostępu w tym czasie.Blokady są używane w każdej sytuacji wymagającej synchronizacji , nie sądzę, abyśmy mogli to określić w bardziej ogólny sposób.
źródło
wait
funkcja zmiennej warunku zwalnia blokadę wewnątrz wywołania i powraca dopiero po ponownym uzyskaniu blokady. po czym możesz bezpiecznie sprawdzić swój stan, ponieważ masz, powiedzmy, „prawa do czytania”. jeśli nadal nie jest to, na co czekasz, wróć dowait
. to jest wzór. btw, ten przykład NIE szanuje tego.W niektórych przypadkach, gdy cv może być zajęte (zablokowane) przez inne wątki. Musisz uzyskać blokadę i zwolnić ją przed powiadomieniem _ * ().
Jeśli nie, powiadomienie _ * () może w ogóle nie zostać wykonane.
źródło
Dodam tylko tę odpowiedź, ponieważ uważam, że zaakceptowana odpowiedź może wprowadzać w błąd. We wszystkich przypadkach będziesz musiał zablokować mutex przed wywołaniem notyfikacji w dowolnym miejscu, aby kod był bezpieczny dla wątków, chociaż możesz odblokować go ponownie przed wywołaniem notyfikacji _ * ().
Aby wyjaśnić, MUSISZ założyć blokadę przed wprowadzeniem wait (lk), ponieważ wait () odblokowuje lk i byłoby to niezdefiniowane zachowanie, gdyby zamek nie był zamknięty. Tak nie jest w przypadku notify_one (), ale musisz się upewnić, że nie wywołasz notyfikacji _ * () przed wprowadzeniem wait () i odblokowaniem muteksu; co oczywiście można zrobić tylko przez zablokowanie tego samego muteksu przed wywołaniem notify _ * ().
Na przykład rozważmy następujący przypadek:
std::atomic_int count; std::mutex cancel_mutex; std::condition_variable cancel_cv; void stop() { if (count.fetch_sub(1) == -999) // Reached -1000 ? cv.notify_one(); } bool start() { if (count.fetch_add(1) >= 0) return true; // Failure. stop(); return false; } void cancel() { if (count.fetch_sub(1000) == 0) // Reached -1000? return; // Wait till count reached -1000. std::unique_lock<std::mutex> lk(cancel_mutex); cancel_cv.wait(lk); }
Ostrzeżenie : ten kod zawiera błąd.
Pomysł jest następujący: wątki wywołują start () i stop () parami, ale tylko tak długo, jak długo start () zwrócił true. Na przykład:
if (start()) { // Do stuff stop(); }
Jeden (inny) wątek w pewnym momencie wywoła funkcję cancel (), a po powrocie z Cancel () zniszczy obiekty, które są potrzebne przy 'Do stuff'. Jednak metoda cancel () nie powinna powracać, gdy między start () a stop () istnieją wątki, a po wykonaniu pierwszej linii przez anulowanie () start () zawsze zwróci wartość false, więc żadne nowe wątki nie wejdą w 'Do obszar rzeczy.
Działa dobrze?
Rozumowanie jest następujące:
1) Jeśli jakikolwiek wątek pomyślnie wykona pierwszą linię funkcji start () (i dlatego zwróci wartość true), to żaden wątek nie wykonał jeszcze pierwszej linii funkcji cancel () (zakładamy, że całkowita liczba wątków jest znacznie mniejsza niż 1000 o wartość sposób).
2) Ponadto, jeśli wątek pomyślnie wykonał pierwszą linię funkcji start (), ale nie wykonał jeszcze pierwszej linii funkcji stop (), nie jest możliwe, aby jakikolwiek wątek pomyślnie wykonał pierwszą linię funkcji cancel () (zwróć uwagę, że tylko jeden wątek kiedykolwiek wywołuje anulowanie ()): wartość zwrócona przez fetch_sub (1000) będzie większa niż 0.
3) Gdy wątek wykona pierwszą linię funkcji cancel (), pierwsza linia start () zawsze zwróci false, a wątek wywołujący start () nie będzie już wchodził do obszaru „Do stuff”.
4) Liczba wywołań start () i stop () jest zawsze zrównoważona, więc po nieudanym wykonaniu pierwszej linii cancel () zawsze będzie moment, w którym (ostatnie) wywołanie stop () spowoduje licznik aby osiągnąć wartość -1000, a zatem powiadomienie_one () zostanie wywołane. Zauważ, że może się to zdarzyć tylko wtedy, gdy pierwsza linia anulowania spowodowała przerwanie tego wątku.
Oprócz problemu z głodem, w którym tak wiele wątków wywołuje start () / stop (), że count nigdy nie osiąga -1000, a cancel () nigdy nie zwraca, co można uznać za „mało prawdopodobne i nigdy nie trwające długo”, jest jeszcze jeden błąd:
Możliwe, że w obszarze „Do stuff” znajduje się jeden wątek, powiedzmy, że wywołuje stop (); w tym momencie wątek wykonuje pierwszą linię anulowania () odczytując wartość 1 za pomocą funkcji fetch_sub (1000) i przechodząc. Ale zanim zajmie mutex i / lub wykona wywołanie wait (lk), pierwszy wątek wykonuje pierwszą linię stop (), odczytuje -999 i wywołuje cv.notify_one ()!
Następnie wywołanie notify_one () jest wykonywane ZANIM czekamy () na zmienną warunku! Program byłby zablokowany na czas nieokreślony.
Z tego powodu nie powinniśmy być w stanie wywołać notify_one (), dopóki nie wywołamy wait (). Zwróć uwagę, że siła zmiennej warunkowej polega na tym, że jest ona w stanie atomowo odblokować muteks, sprawdzić, czy nastąpiło wywołanie notify_one () i iść spać, czy nie. Nie można oszukać go, ale zrobić trzeba zachować mutex zablokowana w dowolnym momencie wprowadzić zmiany do zmiennych, które mogą zmienić stan z false na true i utrzymują go zablokowana podczas wywoływania notify_one (), ponieważ w warunkach wyścigowych jak opisano tutaj.
W tym przykładzie nie ma jednak żadnego warunku. Dlaczego nie użyłem jako warunku „count == -1000”? Ponieważ nie jest to wcale interesujące: gdy tylko osiągnie wartość -1000, jesteśmy pewni, że żaden nowy wątek nie wejdzie do obszaru „Do stuff”. Co więcej, wątki mogą nadal wywoływać start () i zwiększać licznik (do -999 i -998 itd.), Ale nas to nie obchodzi. Liczy się tylko to, że osiągnięto -1000 - dzięki czemu wiemy na pewno, że w obszarze „Do stuff” nie ma już wątków. Jesteśmy pewni, że tak jest, gdy wywoływana jest funkcja notify_one (), ale jak się upewnić, że nie wywołujemy funkcji notify_one (), zanim cancel () zablokowała swój mutex? Samo zablokowanie cancel_mutex na krótko przed notify_one () oczywiście nie pomoże.
Problem w tym, że mimo że nie czekamy na stan, to jest stan i musimy zablokować muteks
1) zanim ten warunek zostanie osiągnięty 2) przed wywołaniem notify_one.
Dlatego prawidłowy kod to:
void stop() { if (count.fetch_sub(1) == -999) // Reached -1000 ? { cancel_mutex.lock(); cancel_mutex.unlock(); cv.notify_one(); } }
[... ten sam początek () ...]
void cancel() { std::unique_lock<std::mutex> lk(cancel_mutex); if (count.fetch_sub(1000) == 0) return; cancel_cv.wait(lk); }
Oczywiście to tylko jeden przykład, ale inne przypadki są bardzo podobne; prawie we wszystkich przypadkach, w których używasz zmiennej warunkowej, będziesz musiał zablokować ten mutex (na krótko) przed wywołaniem notify_one (), w przeciwnym razie możliwe jest, że wywołasz ją przed wywołaniem wait ().
Zwróć uwagę, że odblokowałem mutex przed wywołaniem notify_one () w tym przypadku, ponieważ w przeciwnym razie istnieje (mała) szansa, że wywołanie notify_one () obudzi wątek oczekujący na zmienną warunku, która następnie spróbuje przejąć muteks i blok, zanim ponownie zwolnimy muteks. To tylko trochę wolniej niż potrzeba.
Ten przykład był wyjątkowy, ponieważ wiersz zmieniający warunek jest wykonywany przez ten sam wątek, który wywołuje funkcję wait ().
Bardziej typowy jest przypadek, w którym jeden wątek po prostu czeka, aż warunek stanie się prawdziwy, a inny wątek przejmuje blokadę przed zmianą zmiennych związanych z tym warunkiem (powodując, że prawdopodobnie stanie się on prawdziwy). W takim przypadku mutex jest blokowany bezpośrednio przed (i po) spełnieniu warunku - więc w takim przypadku można po prostu odblokować muteks przed wywołaniem notyfikacji _ * ().
źródło