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)
cv.notify_one();
}
bool start()
{
if (count.fetch_add(1) >= 0)
return true;
stop();
return false;
}
void cancel()
{
if (count.fetch_sub(1000) == 0)
return;
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())
{
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)
{
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 _ * ().
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.