Je suis un peu confus quant à l’utilisation de std::condition_variable
. Je comprends que je dois créer un unique_lock
sur un mutex
avant d’appeler condition_variable.wait()
. Ce que je ne peux pas trouver, c’est si je devrais aussi acquérir un verrou unique avant d’appeler notify_one()
ou notify_all()
.
Les exemples sur cppreference.com sont en conflit. Par exemple, la page notify_one donne cet exemple:
#include #include #include #include std::condition_variable cv; std::mutex cv_m; int i = 0; bool done = false; void waits() { std::unique_lock 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 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(); }
Ici, le verrou n’est pas acquis pour le premier notify_one()
, mais est acquis pour le second notify_one()
. En regardant d’autres pages avec des exemples, je vois différentes choses, la plupart du temps ne pas acquérir la serrure.
notify_one()
, et pourquoi choisirais-je de le verrouiller? notify_one()
, mais pour les appels suivants. Cet exemple est-il faux ou existe-t-il une justification? Vous n’avez pas besoin de tenir un verrou lorsque vous appelez condition_variable::notify_one()
, mais ce n’est pas faux dans le sens où c’est toujours un comportement bien défini et non une erreur.
Cependant, cela peut être une “pessimisation” car tout thread en attente est exécuté (le cas échéant) et essaiera immédiatement d’acquérir le verrou que le thread notifiant détient. Je pense que c’est une bonne règle de base pour éviter de maintenir le verrou associé à une variable de condition en appelant notify_one()
ou notify_all()
. Voir Pthread Mutex: pthread_mutex_unlock () consum beaucoup de temps pour un exemple où la libération d’un verrou avant d’appeler l’équivalent notify_one()
de notify_one()
améliore les performances de manière mesurable.
Gardez à l’esprit que l’appel lock()
dans la boucle while est nécessaire à un moment donné, car le verrou doit être maintenu pendant la vérification de la condition while while (!done)
boucle. Mais il n’est pas nécessaire de le conserver pour l’appel à notify_one()
.
2016-02-27 : Mise à jour importante pour répondre à certaines questions dans les commentaires sur l’existence d’une condition de concurrence : le verrou n’est pas une aide pour l’appel notify_one()
. Je sais que cette mise à jour est en retard car la question a été posée il y a presque deux ans, mais je voudrais répondre à la question de @ Cookie concernant une éventuelle condition de notify_one()
si le producteur ( signals()
dans cet exemple) appelle notify_one()
juste avant le consommateur. ( waits()
dans cet exemple) est capable d’appeler wait()
.
La clé est ce qui arrive à i
– c’est l’object qui indique réellement si le consommateur a “du travail” à faire. La condition_variable
n’est qu’un mécanisme permettant au consommateur d’attendre efficacement une modification de i
.
Le producteur doit conserver le verrou lors de la mise à jour de i
, et le consommateur doit maintenir le verrou tout en vérifiant i
et en appelant condition_variable::wait()
(s’il doit attendre). Dans ce cas, la clé est qu’il doit s’agir de la même instance de maintien du verrou (souvent appelée section critique) lorsque le consommateur effectue cette vérification. Étant donné que la section critique est conservée lorsque le producteur met à jour i
et que le consommateur attend et attend sur i
, il n’ya pas de possibilité de changer lorsque le consommateur vérifie i
et qu’il appelle condition_variable::wait()
. C’est le point crucial pour une utilisation correcte des variables de condition.
Le standard C ++ dit que condition_variable :: wait () se comporte comme suit lorsqu’il est appelé avec un prédicat (comme dans ce cas):
while (!pred()) wait(lock);
Deux situations peuvent se produire lorsque le consommateur vérifie i
:
Si i
est cv.wait()
0, le consommateur appelle cv.wait()
, alors i
serai toujours à 0 lorsque la partie wait(lock)
de l’implémentation est appelée – l’utilisation correcte des verrous le garantit. Dans ce cas, le producteur n’a pas la possibilité d’appeler la condition_variable::notify_one()
dans sa boucle while jusqu’à ce que le consommateur ait appelé cv.wait(lk, []{return i == 1;})
(et le wait()
call a fait tout ce qu’il faut pour attraper correctement un notification – wait()
ne libère pas le verrou tant que cela n’a pas été fait. Donc, dans ce cas, le consommateur ne peut pas manquer la notification.
Si i
est déjà 1 lorsque le consommateur appelle cv.wait()
, la partie wait(lock)
de l’implémentation ne sera jamais appelée car le test while (!pred())
provoquera l’arrêt de la boucle interne. Dans cette situation, peu importe que l’appel à notify_one () se produise – le consommateur ne bloquera pas.
L’exemple ici présente la complexité supplémentaire d’utiliser la variable done
pour signaler au thread producteur que le consommateur a reconnu que i == 1
, mais je ne pense pas que cela change l’parsing du tout car tous les access à done
(à la fois pour la lecture et la modification) sont effectuées dans les mêmes sections critiques qui impliquent i
et la condition_variable
.
Si vous regardez la question à laquelle @ eh9 a fait référence, Sync n’est pas fiable en utilisant std :: atomic et std :: condition_variable , vous verrez une condition de concurrence. Toutefois, le code publié dans cette question enfreint l’une des règles fondamentales d’utilisation d’une variable de condition: il ne contient pas une seule section critique lors d’une vérification et d’une attente.
Dans cet exemple, le code ressemble à:
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 lock(f->resume_mutex); f->resume.wait(lock); // (3) }
Vous remarquerez que l’ wait()
à # 3 est effectuée en maintenant f->resume_mutex
. Mais la vérification pour savoir si wait()
est nécessaire à l’étape 1 n’est pas effectuée en conservant ce verrou (beaucoup moins continuellement pour la vérification et l’attente), ce qui est nécessaire pour utiliser correctement les variables de condition. . Je crois que la personne qui a le problème avec cet extrait de code pensait que puisque f->counter
était un type std::atomic
cela répondrait à l’exigence. Cependant, l’atomicité fournie par std::atomic
ne s’étend pas à l’appel suivant à f->resume.wait(lock)
. Dans cet exemple, il y a une course entre le moment où le f->counter
est vérifié (étape n ° 1) et celui où l’appel wait()
est appelé (étape n ° 3).
Cette race n’existe pas dans l’exemple de cette question.
En utilisant vc10 et Boost 1.56, j’ai mis en place une queue concurrente, à la manière de cet article de blog . L’auteur déverrouille le mutex pour minimiser les notify_one()
, c’est-à-dire que notify_one()
est appelé avec le mutex déverrouillé:
void push(const T& item) { std::unique_lock mlock(mutex_); queue_.push(item); mlock.unlock(); // unlock before notificiation to minimize mutex contention cond_.notify_one(); // notify one waiting thread }
Le délocking du mutex s’appuie sur un exemple de la documentation Boost :
void prepare_data_for_processing() { resortingeve_data(); prepare_data(); { boost::lock_guard lock(mut); data_ready=true; } cond.notify_one(); }
Cela a néanmoins conduit au comportement erratique suivant:
notify_one()
n’a pas encore été appelé cond_.wait()
peut toujours être interrompu via boost::thread::interrupt()
notify_one()
été appelé pour la première fois les cond_.wait()
; l’attente ne peut plus être arrêtée par boost::thread::interrupt()
ou boost::condition_variable::notify_*()
. La suppression de la ligne mlock.unlock()
permis au code de fonctionner comme prévu (les notifications et les interruptions mlock.unlock()
fin à l’attente). Notez que notify_one()
est appelé avec le mutex toujours verrouillé, il est déverrouillé immédiatement après avoir quitté la scope:
void push(const T& item) { std::lock_guard mlock(mutex_); queue_.push(item); cond_.notify_one(); // notify one waiting thread }
Cela signifie qu’au moins avec mon implémentation de thread particulière, le mutex ne doit pas être déverrouillé avant d’appeler boost::condition_variable::notify_one()
, bien que les deux méthodes semblent correctes.
@ Michael Burr est correct. condition_variable::notify_one
ne nécessite pas de verrou sur la variable. Rien ne vous empêche cependant d’utiliser un verrou dans cette situation, comme l’illustre l’exemple.
Dans l’exemple donné, le verrou est motivé par l’utilisation simultanée de la variable i
. Étant donné que le thread de signals
modifie la variable, il doit s’assurer qu’aucun autre thread n’y a access au cours de cette période.
Les verrous sont utilisés pour toute situation nécessitant une synchronisation , je ne pense pas que nous pouvons le dire d’une manière plus générale.
Dans certains cas, lorsque le CV peut être occupé (verrouillé) par d’autres threads. Vous devez obtenir le locking et le libérer avant d’avertir _ * ().
Si non, le notifications _ * () peut ne pas être exécuté du tout.