Czy muszę uzyskać blokadę przed wywołaniem condition_variable.notify_one ()?


90

Jestem trochę zdezorientowany co do użycia std::condition_variable. Rozumiem, że trzeba stworzyć unique_lockna zasadzie mutexprzed wywołaniem condition_variable.wait(). Nie mogę znaleźć, czy powinienem również uzyskać unikalną blokadę przed wywołaniem, notify_one()czy notify_all().

Przykłady na cppreference.com są sprzeczne. Na przykład strona notify_one podaje następujący przykład:

#include <iostream>
#include <condition_variable>
#include <thread>
#include <chrono>

std::condition_variable cv;
std::mutex cv_m;
int i = 0;
bool done = false;

void waits()
{
    std::unique_lock<std::mutex> lk(cv_m);
    std::cout << "Waiting... \n";
    cv.wait(lk, []{return i == 1;});
    std::cout << "...finished waiting. i == 1\n";
    done = true;
}

void signals()
{
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "Notifying...\n";
    cv.notify_one();

    std::unique_lock<std::mutex> lk(cv_m);
    i = 1;
    while (!done) {
        lk.unlock();
        std::this_thread::sleep_for(std::chrono::seconds(1));
        lk.lock();
        std::cerr << "Notifying again...\n";
        cv.notify_one();
    }
}

int main()
{
    std::thread t1(waits), t2(signals);
    t1.join(); t2.join();
}

Tutaj zamek nie jest pozyskiwany za pierwszy notify_one(), ale za drugi notify_one(). Przeglądając inne strony z przykładami, widzę różne rzeczy, głównie nie zdobywam blokady.

  • Czy mogę samodzielnie zablokować muteks przed wywołaniem notify_one()i dlaczego miałbym go zablokować?
  • W podanym przykładzie dlaczego nie ma blokady dla pierwszego notify_one(), ale jest dla kolejnych wywołań. Czy ten przykład jest błędny, czy jest jakieś uzasadnienie?

Odpowiedzi:


77

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 whilepę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_variabletylko mechanizm pozwalający konsumentowi efektywnie czekać na zmianę i.

Producent musi trzymać zamek podczas aktualizacji i, a konsument musi przytrzymać zamek podczas sprawdzania ii 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 ii gdy konsument sprawdza i czeka i, nie ma możliwości izmiany między momentem sprawdzenia ia 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 iwynosi 0, to klient dzwoni cv.wait(), to inadal 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 whilepę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 imomencie 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 donezmiennej 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ą ii 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)      // (1)
    // we have zeroed this fence's counter, wake up everyone that waits
    f->resume.notify_all(); // (2)
else
{
    unique_lock<mutex> lock(f->resume_mutex);
    f->resume.wait(lock);   // (3)
}

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->counterbył to std::atomictyp, spełniłby to wymaganie. Jednak atomowość zapewniana przez std::atomicnie obejmuje kolejnego wywołania f->resume.wait(lock). W tym przykładzie istnieje wyścig między momentem f->counterzaznaczenia (krok 1) a wait()wywołaniem (krok 3).

Ta rasa nie istnieje w przykładzie tego pytania.


2
ma to głębsze implikacje: domaigne.com/blog/computing/ ... Warto zauważyć, że wspomniany wcześniej problem powinien zostać rozwiązany albo przez nowszą wersję, albo przez wersję zbudowaną z poprawnymi flagami. (aby włączyć wait morphingoptymalizację) 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.
v.oddou,

6
@Michael: W moim rozumieniu konsument musi w końcu zadzwonić 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ę ...
Cookie

1
np. powiedz w powyższym kodzie, w którym znajduje się konsument, podczas std::cout << "Waiting... \n";gdy producent to robi cv.notify_one();, wtedy znika sygnał budzenia ... A może coś mi tu brakuje?
Cookie

1
@Ciastko. Tak, tam są warunki wyścigu. Zobacz stackoverflow.com/questions/20982270/…
eh9

1
@ eh9: Cholera, właśnie znalazłem przyczynę błędu, który od czasu do czasu blokował mój kod dzięki twojemu komentarzowi. Było to spowodowane dokładnie tym przypadkiem stanu wyścigu. Odblokowanie mutexa po powiadomieniu w pełni rozwiązało problem ... Wielkie dzięki!
galinette

10

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:

  • while notify_one()jeszcze nie został wywołany, cond_.wait()nadal można go przerwać za pomocąboost::thread::interrupt()
  • raz notify_one()został wezwany po raz pierwszy do cond_.wait()impasu; czekania nie można już zakończyć boost::thread::interrupt()ani boost::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ż, że notify_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.


Czy zgłosiłeś ten problem do Boost.Thread? Nie mogę znaleźć tam podobnego zadania svn.boost.org/trac/boost/ ...
magras

@magras Niestety nie, nie mam pojęcia, dlaczego o tym nie pomyślałem. I niestety nie udaje mi się odtworzyć tego błędu przy użyciu wspomnianej kolejki.
Matthäus Brandl

Nie wiem, jak wczesne budzenie może spowodować impas. W szczególności, jeśli wyjdziesz z cond_.wait () w pop () po tym, jak push () zwolni kolejkę mutex, ale przed wywołaniem notify_one () - Pop () powinno zobaczyć kolejkę niepustą i wykorzystać nowy wpis zamiast Czekanie. jeśli wyjdziesz z cond_.wait (), podczas gdy push () aktualizuje kolejkę, blokada powinna być utrzymywana przez push (), więc pop () powinno blokować oczekiwanie na zwolnienie blokady. Wszelkie inne wczesne wybudzania zatrzymywałyby blokadę, uniemożliwiając push () modyfikowanie kolejki przed wywołaniem przez pop () następnego wait (). Co mnie ominęło?
Kevin

4

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 zapobiec condition_variablezniszczeniu przed notify_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 tpo jego utworzeniu, ale zanim zaczniemy czekać na zmienną warunku (gdzieś pomiędzy (5) a (6)). Wątek tuzyskuje blokadę (1), ustawia zmienną predykatu (2), a następnie zwalnia blokadę (3). Załóżmy, że w tym momencie istnieje inny przełącznik kontekstu przed wykonaniem notify_one()(4). Wątek główny przejmuje blokadę (6) i wykonuje wiersz (7), w którym to momencie predykat powraca truei nie ma powodu czekać, więc zwalnia blokadę i kontynuuje. foozwraca (8) i zmienne w swoim zakresie (w tym (4), w którym to momencie jest już zniszczone!cv ) są niszczone. Zanim wątek tmó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, że twywołania wątku notify_onewcześniej będą cv.waitmogły sprawdzić nowo ustawioną zmienną predykatu i kontynuować, ponieważ w t celu wykonania sprawdzenia musiałaby uzyskać blokadę, która jest obecnie utrzymywana. Tak więc zapewniamy, że po powrocie cvnie ma do niego dostępu wątek .tfoo

Podsumowując, problem w tym konkretnym przypadku nie dotyczy tak naprawdę wątków, ale czasów życia zmiennych przechwyconych przez odniesienie. cvjest przechwytywany przez odniesienie za pośrednictwem wątku t, dlatego musisz upewnić się, że cvpozostaje żywy przez czas wykonywania wątku. Inne przedstawione tutaj przykłady nie cierpią z powodu tego problemu, ponieważ obiekty condition_variablei mutexsą zdefiniowane w zasięgu globalnym, dlatego gwarantuje się, że pozostaną one żywe do momentu zakończenia programu.


1

@Michael Burr ma rację. condition_variable::notify_onenie 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ż signalswą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.


oczywiście, ale poza tym muszą być również używane w połączeniu ze zmiennymi warunkowymi, aby cały wzorzec faktycznie działał. w szczególności waitfunkcja 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óć do wait. to jest wzór. btw, ten przykład NIE szanuje tego.
v.oddou,

1

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.


1

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 _ * ().

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.