Devo acquisire il blocco prima di chiamare condition_variable.notify_one ()?


90

Sono un po 'confuso sull'uso di std::condition_variable. Capisco che devo creare un unique_locksu a mutexprima di chiamare condition_variable.wait(). Quello che non riesco a trovare è se devo anche acquisire un blocco univoco prima di chiamare notify_one()o notify_all().

Gli esempi su cppreference.com sono in conflitto. Ad esempio, la pagina notify_one fornisce questo esempio:

#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();
}

Qui il lucchetto non viene acquisito per il primo notify_one(), ma viene acquisito per il secondo notify_one(). Guardando altre pagine con esempi vedo cose diverse, per lo più non acquisendo il lucchetto.

  • Posso scegliere da solo di bloccare il mutex prima di chiamare notify_one()e perché dovrei scegliere di bloccarlo?
  • Nell'esempio fornito, perché non c'è il blocco per il primo notify_one(), ma c'è per le chiamate successive. Questo esempio è sbagliato o c'è qualche logica?

Risposte:


77

Non è necessario tenere premuto un lucchetto durante la chiamata condition_variable::notify_one(), ma non è sbagliato nel senso che è comunque un comportamento ben definito e non un errore.

Tuttavia, potrebbe trattarsi di una "pessimizzazione" poiché qualsiasi thread in attesa viene reso eseguibile (se presente) tenterà immediatamente di acquisire il blocco detenuto dal thread di notifica. Penso che sia una buona regola pratica evitare di mantenere il blocco associato a una variabile di condizione durante la chiamata notify_one()o notify_all(). Vedere Pthread Mutex: pthread_mutex_unlock () consuma molto tempo per un esempio in cui rilasciare un blocco prima di chiamare l'equivalente pthread di notify_one()prestazioni migliorate misurabili.

Tieni presente che la lock()chiamata nel whileciclo è necessaria a un certo punto, perché il blocco deve essere mantenuto durante il while (!done)controllo delle condizioni del ciclo. Ma non è necessario trattenerlo per la chiamata a notify_one().


27/02/2016 : Ampio aggiornamento per rispondere ad alcune domande nei commenti sull'eventuale presenza di una race condition se il blocco non è di aiuto per la notify_one()chiamata. So che questo aggiornamento è in ritardo perché la domanda è stata posta quasi due anni fa, ma vorrei rispondere alla domanda di @ Cookie su una possibile condizione di gara se il produttore ( signals()in questo esempio) chiama notify_one()appena prima che il consumatore ( waits()in questo esempio) sia in grado di chiamare wait().

La chiave è ciò che accade a i- questo è l'oggetto che effettivamente indica se il consumatore ha o meno "lavoro" da fare. Il condition_variableè solo un meccanismo per consentire al consumatore in modo efficiente attendere una modifica i.

Il produttore deve tenere il blocco durante l'aggiornamento ie il consumatore deve tenere il blocco durante il controllo ie la chiamata condition_variable::wait()(se deve aspettare). In questo caso, la chiave è che deve essere la stessa istanza in cui si tiene il lucchetto (spesso chiamato sezione critica) quando il consumatore esegue questo controllo e attesa. Poiché la sezione critica si tiene quando il produttore si aggiorna ie quando il consumatore controlla e attende i, non c'è possibilità idi cambiare tra quando il consumatore controlla ie quando chiama condition_variable::wait(). Questo è il punto cruciale per un uso corretto delle variabili di condizione.

Lo standard C ++ dice che condition_variable :: wait () si comporta come segue quando viene chiamato con un predicato (come in questo caso):

while (!pred())
    wait(lock);

Ci sono due situazioni che possono verificarsi quando il consumatore controlla i:

  • se iè 0, il consumatore chiama cv.wait(), quindi isarà ancora 0 quando wait(lock)viene chiamata la parte dell'implementazione: l'uso corretto dei blocchi lo garantisce. In questo caso, il produttore non ha la possibilità di chiamare condition_variable::notify_one()nel suo whileciclo fino a quando il consumatore non ha chiamato cv.wait(lk, []{return i == 1;})(e la wait()chiamata ha fatto tutto ciò che deve fare per `` catturare '' correttamente una notifica) wait()non rilascia il blocco finché non lo ha fatto ). Quindi in questo caso il consumatore non può perdere la notifica.

  • se iè già 1 quando il consumatore chiama cv.wait(), la wait(lock)parte dell'implementazione non verrà mai chiamata perché il while (!pred())test farà terminare il ciclo interno. In questa situazione non importa quando si verifica la chiamata a notify_one (): il consumatore non si bloccherà.

L'esempio qui ha l'ulteriore complessità di utilizzare la donevariabile per segnalare al thread del produttore che il consumatore lo ha riconosciuto i == 1, ma non credo che questo cambi affatto l'analisi perché tutto l'accesso a done(sia per la lettura che per la modifica ) vengono svolte nelle stesse sezioni critiche che coinvolgono ie il condition_variable.

Se si guarda alla domanda che @ EH9 indicò, Sync è inaffidabile utilizzando std :: atomica e std :: condition_variable , si potranno vedere una condizione di competizione. Tuttavia, il codice pubblicato in quella domanda viola una delle regole fondamentali dell'utilizzo di una variabile di condizione: non contiene una singola sezione critica quando si esegue un check-and-wait.

In questo esempio, il codice ha il seguente aspetto:

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)
}

Noterai che il punto wait()3 viene eseguito tenendo premuto f->resume_mutex. Ma il controllo per verificare se wait()è necessario o meno al passaggio # 1 non viene eseguito tenendo premuto quel blocco (molto meno continuamente per il check-and-wait), che è un requisito per un uso corretto delle variabili di condizione). Credo che la persona che ha il problema con quello snippet di codice abbia pensato che poiché f->counterera un std::atomictipo questo avrebbe soddisfatto il requisito. Tuttavia, l'atomicità fornita da std::atomicnon si estende alla successiva chiamata a f->resume.wait(lock). In questo esempio, c'è una corsa tra il momento in cui f->counterè selezionato (passaggio # 1) e quando wait()viene chiamato (passaggio # 3).

Quella razza non esiste nell'esempio di questa domanda.


2
ha implicazioni più profonde: domaigne.com/blog/computing/… In particolare, il problema di pthread menzionato dovrebbe essere risolto da una versione più recente o da una versione costruita con i flag corretti. (per abilitare l' wait morphingottimizzazione) Regola empirica spiegata in questo collegamento: notificare WITH lock è meglio in situazioni con più di 2 thread per risultati più prevedibili.
v.oddou

6
@ Michael: per quanto ne so, il consumatore deve eventualmente chiamare the_condition_variable.wait(lock);. Se non è necessario alcun blocco per sincronizzare produttore e consumatore (supponiamo che il sottostante sia una coda spsc priva di blocco), quel blocco non ha scopo se il produttore non lo blocca. Per me andava bene. Ma non c'è il rischio per una razza rara? Se il produttore non tiene il lucchetto, non potrebbe chiamare notify_one mentre il consumatore è appena prima dell'attesa? Quindi il consumatore attacca e non si sveglia ...
Cookie

1
ad esempio, dì nel codice sopra il consumatore std::cout << "Waiting... \n";mentre lo fa il produttore cv.notify_one();, quindi la sveglia scompare ... O mi manca qualcosa qui?
Cookie

1
@Cookie. Sì, c'è una condizione di gara lì. Vedere stackoverflow.com/questions/20982270/...
EH9

1
@ eh9: Dannazione, ho appena scoperto la causa di un bug che di tanto in tanto congelava il mio codice grazie al tuo commento. Era dovuto a questo caso esatto di condizioni di gara. Lo sblocco del mutex dopo la notifica ha risolto completamente il problema ... Grazie mille!
galinette

10

Situazione

Utilizzando vc10 e Boost 1.56 ho implementato una coda simultanea più o meno come suggerisce questo post del blog . L'autore sblocca il mutex per ridurre al minimo la contesa, ovvero notify_one()viene chiamato con il mutex sbloccato:

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
}

Lo sblocco del mutex è supportato da un esempio nella documentazione di Boost :

void prepare_data_for_processing()
{
    retrieve_data();
    prepare_data();
    {
        boost::lock_guard<boost::mutex> lock(mut);
        data_ready=true;
    }
    cond.notify_one();
}

Problema

Tuttavia questo ha portato al seguente comportamento irregolare:

  • mentre notify_one()è non è stato chiamato ancora cond_.wait()può ancora essere interrotta tramiteboost::thread::interrupt()
  • una volta è notify_one()stato chiamato per la prima volta cond_.wait()deadlock; l'attesa non può essere terminata da boost::thread::interrupt()o boost::condition_variable::notify_*()più.

Soluzione

La rimozione della riga ha mlock.unlock()fatto funzionare il codice come previsto (notifiche e interruzioni terminano l'attesa). Nota che notify_one()viene chiamato con il mutex ancora bloccato, viene sbloccato subito dopo quando si lascia l'ambito:

void push(const T& item)
{
  std::lock_guard<std::mutex> mlock(mutex_);
  queue_.push(item);
  cond_.notify_one(); // notify one waiting thread
}

Ciò significa che almeno con la mia particolare implementazione del thread il mutex non deve essere sbloccato prima della chiamata boost::condition_variable::notify_one(), sebbene entrambi i modi sembrino corretti.


Hai segnalato questo problema a Boost.Thread? Non riesco a trovare un'attività simile lì svn.boost.org/trac/boost/…
magras

@magras Purtroppo non l'ho fatto, non ho idea del motivo per cui non l'ho considerato. E purtroppo non riesco a riprodurre questo errore utilizzando la coda menzionata.
Matthäus Brandl

Non sono sicuro di vedere come il risveglio precoce possa causare una situazione di stallo. In particolare, se esci da cond_.wait () in pop () dopo che push () ha rilasciato il mutex della coda ma prima che venga chiamato notify_one () - Pop () dovrebbe vedere la coda non vuota e consumare la nuova voce invece di in attesa. se esci da cond_.wait () mentre push () sta aggiornando la coda, il blocco dovrebbe essere mantenuto da push (), quindi pop () dovrebbe bloccarsi in attesa che il blocco venga rilasciato. Qualsiasi altro risveglio iniziale manterrebbe il blocco, impedendo a push () di modificare la coda prima che pop () chiami il successivo wait (). Cosa mi sono perso?
Kevin

4

Come altri hanno sottolineato, non è necessario tenere premuto il blocco durante la chiamata notify_one(), in termini di condizioni di gara e problemi relativi al threading. Tuttavia, in alcuni casi, potrebbe essere necessario tenere premuto il lucchetto per evitare che condition_variablevenga distrutto prima che notify_one()venga chiamato. Considera il seguente esempio:

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)
}

Supponiamo che ci sia un cambio di contesto per il thread appena creato tdopo averlo creato ma prima di iniziare ad aspettare la variabile di condizione (da qualche parte tra (5) e (6)). Il thread tacquisisce il blocco (1), imposta la variabile del predicato (2) e quindi rilascia il blocco (3). Supponiamo che ci sia un altro cambio di contesto proprio a questo punto prima che notify_one()(4) venga eseguito. Il thread principale acquisisce il lock (6) ed esegue la riga (7), a quel punto il predicato ritorna truee non c'è motivo di aspettare, quindi rilascia il lock e continua. foorestituisce (8) e le variabili nel suo ambito (incluse cv) vengono distrutte. Prima che il thread tpossa unirsi al thread principale (9), deve terminare la sua esecuzione, quindi continua da dove era stato interrotto per eseguirecv.notify_one()(4), a quel punto cvè già distrutto!

La possibile soluzione in questo caso è mantenere il blocco durante la chiamata notify_one(ovvero rimuovere l'ambito che termina con la riga (3)). In questo modo, ci assicuriamo che le tchiamate ai thread notify_oneprima cv.waitpossano controllare la variabile del predicato appena impostata e continuare, poiché t per eseguire il controllo sarebbe necessario acquisire il blocco, che è attualmente in possesso. Quindi, ci assicuriamo che cvnon sia accessibile dal thread tdopo i fooritorni.

Per riassumere, il problema in questo caso specifico non è proprio il threading, ma la durata delle variabili catturate per riferimento. cvviene catturato per riferimento tramite thread t, quindi devi assicurarti che cvrimanga attivo per la durata dell'esecuzione del thread. Gli altri esempi presentati qui non soffrono di questo problema, perché condition_variablee gli mutexoggetti sono definiti nell'ambito globale, quindi è garantito che siano mantenuti in vita fino alla chiusura del programma.


1

@Michael Burr ha ragione. condition_variable::notify_onenon richiede un blocco sulla variabile. Niente ti impedisce di usare un lucchetto in quella situazione, come mostra l'esempio.

Nell'esempio fornito, il blocco è motivato dall'uso simultaneo della variabile i. Poiché il signalsthread modifica la variabile, è necessario assicurarsi che nessun altro thread vi acceda durante quel periodo.

I lucchetti sono usati per qualsiasi situazione che richieda la sincronizzazione , non credo che si possa affermare in modo più generale.


ovviamente, ma oltre a questo devono anche essere usati in congiunzione con variabili di condizione in modo che l'intero pattern funzioni effettivamente. in particolare, la waitfunzione della variabile di condizione rilascia il blocco all'interno della chiamata e ritorna solo dopo aver riacquistato il blocco. dopodiché puoi tranquillamente verificare le tue condizioni perché hai acquisito i "diritti di lettura" diciamo. se non è ancora quello che stai aspettando, torna a wait. questo è lo schema. btw, questo esempio NON lo rispetta.
v.oddou

1

In alcuni casi, quando il cv può essere occupato (bloccato) da altri thread. Devi ottenere il blocco e rilasciarlo prima di notificare _ * ().
In caso contrario, la notifica _ * () potrebbe non essere eseguita affatto.


1

Aggiungendo solo questa risposta perché penso che la risposta accettata potrebbe essere fuorviante. In tutti i casi dovrai bloccare il mutex, prima di chiamare notify_one () da qualche parte affinché il tuo codice sia thread-safe, sebbene potresti sbloccarlo di nuovo prima di chiamare effettivamente notify _ * ().

Per chiarire, DEVI prendere il lucchetto prima di entrare in wait (lk) perché wait () sblocca lk e sarebbe un comportamento indefinito se il lucchetto non fosse bloccato. Questo non è il caso di notify_one (), ma devi assicurarti di non chiamare notify _ * () prima di entrare in wait () e fare in modo che quella chiamata sblocchi il mutex; che ovviamente può essere fatto solo bloccando lo stesso mutex prima di chiamare notify _ * ().

Ad esempio, considera il seguente caso:

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);
}

Attenzione : questo codice contiene un bug.

L'idea è la seguente: i thread chiamano start () e stop () in coppia, ma solo finché start () ha restituito true. Per esempio:

if (start())
{
  // Do stuff
  stop();
}

Un (altro) thread ad un certo punto chiamerà cancel () e dopo essere tornato da cancel () distruggerà gli oggetti necessari in "Fai cose". Tuttavia, si suppone che cancel () non ritorni mentre ci sono thread tra start () e stop (), e una volta che cancel () ha eseguito la sua prima riga, start () restituirà sempre false, quindi nessun nuovo thread entrerà nel 'Do area roba.

Funziona bene?

Il ragionamento è il seguente:

1) Se un thread esegue con successo la prima riga di start () (e quindi restituirà true), nessun thread ha ancora eseguito la prima riga di cancel () (assumiamo che il numero totale di thread sia molto modo).

2) Inoltre, mentre un thread ha eseguito con successo la prima riga di start (), ma non ancora la prima riga di stop (), allora è impossibile che qualsiasi thread esegua con successo la prima riga di cancel () (nota che solo un thread ever calls cancel ()): il valore restituito da fetch_sub (1000) sarà maggiore di 0.

3) Una volta che un thread ha eseguito la prima riga di cancel (), la prima riga di start () restituirà sempre false e un thread che chiama start () non entrerà più nell'area "Fai cose".

4) Il numero di chiamate a start () e stop () è sempre bilanciato, quindi dopo che la prima riga di cancel () è stata eseguita senza successo, ci sarà sempre un momento in cui una (l'ultima) chiamata a stop () causa il conteggio per raggiungere -1000 e quindi notify_one () deve essere chiamato. Nota che può accadere solo quando la prima riga di annullamento ha provocato la caduta di quel thread.

A parte un problema di fame in cui così tanti thread chiamano start () / stop () che count non raggiunge mai -1000 e cancel () non ritorna mai, cosa che si potrebbe accettare come "improbabile e che non dura mai a lungo", c'è un altro bug:

È possibile che ci sia un thread all'interno dell'area 'Do stuff', diciamo che sta solo chiamando stop (); in quel momento un thread esegue la prima riga di cancel () leggendo il valore 1 con fetch_sub (1000) e cadendo. Ma prima che prenda il mutex e / o esegua la chiamata ad wait (lk), il primo thread esegue la prima riga di stop (), legge -999 e chiama cv.notify_one ()!

Quindi questa chiamata a notify_one () viene eseguita PRIMA di attendere () sulla variabile di condizione! E il programma si bloccherebbe indefinitamente.

Per questo motivo non dovremmo essere in grado di chiamare notify_one () fino a non chiamiamo wait (). Notare che la potenza di una variabile di condizione sta nel fatto che è in grado di sbloccare atomicamente il mutex, controllare se è avvenuta una chiamata a notify_one () e andare a dormire o meno. Non si può ingannare, ma si fare necessità di mantenere il mutex bloccato ogni volta che si apportano modifiche alle variabili che potrebbero cambiare la condizione da false a true e tenere chiusa a chiave durante la chiamata notify_one () a causa delle condizioni di gara, come descritto qui.

In questo esempio, tuttavia, non è presente alcuna condizione. Perché non ho utilizzato come condizione "count == -1000"? Perché questo non è affatto interessante qui: non appena viene raggiunto -1000, siamo sicuri che nessun nuovo thread entrerà nell'area 'Do stuff'. Inoltre, i thread possono ancora chiamare start () e incrementeranno il conteggio (a -999 e -998 ecc.) Ma non ci interessa. L'unica cosa che conta è che sia stato raggiunto -1000, quindi sappiamo per certo che non ci sono più thread nell'area "Fai cose". Siamo sicuri che questo sia il caso quando viene chiamato notify_one (), ma come assicurarci di non chiamare notify_one () prima che cancel () abbia bloccato il suo mutex? Il semplice blocco di cancel_mutex poco prima di notify_one () non aiuterà ovviamente.

Il problema è che, nonostante non stiamo aspettando una condizione, ci sono ancora è una condizione, e abbiamo bisogno di bloccare il mutex

1) prima che tale condizione sia raggiunta 2) prima di chiamare notify_one.

Il codice corretto diventa quindi:

void stop()
{
  if (count.fetch_sub(1) == -999) // Reached -1000 ?
  {
    cancel_mutex.lock();
    cancel_mutex.unlock();
    cv.notify_one();
  }
}

[... stesso inizio () ...]

void cancel()
{
  std::unique_lock<std::mutex> lk(cancel_mutex);
  if (count.fetch_sub(1000) == 0)
    return;
  cancel_cv.wait(lk);
}

Naturalmente questo è solo un esempio, ma altri casi sono molto simili; in quasi tutti i casi in cui si utilizza una variabile condizionale avrete bisogno di che il mutex sia bloccato (poco) prima di chiamare notify_one (), oppure è possibile che lo chiami prima di chiamare wait ().

Nota che ho sbloccato il mutex prima di chiamare notify_one () in questo caso, perché altrimenti c'è la (piccola) possibilità che la chiamata a notify_one () svegli il thread in attesa della variabile di condizione che poi proverà a prendere il mutex e block, prima di rilasciare nuovamente il mutex. È solo leggermente più lento del necessario.

Questo esempio era un po 'speciale in quanto la riga che cambia la condizione viene eseguita dallo stesso thread che chiama wait ().

Più usuale è il caso in cui un thread aspetta semplicemente che una condizione diventi vera e un altro thread prende il blocco prima di modificare le variabili coinvolte in quella condizione (facendola eventualmente diventare vera). In tal caso, il mutex viene bloccato immediatamente prima (e dopo) che la condizione si sia verificata, quindi è assolutamente ok sbloccare il mutex prima di chiamare notify _ * () in quel caso.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.