Vous n'avez pas besoin de maintenir un verrou lors de l'appel condition_variable::notify_one()
, mais ce n'est pas faux dans le sens où il s'agit toujours d'un comportement bien défini et non d'une erreur.
Cependant, cela pourrait être une "pessimisation" puisque tout thread en attente rendu exécutable (le cas échéant) 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 lors de l'appel notify_one()
ou notify_all()
. Voir Pthread Mutex: pthread_mutex_unlock () consomme beaucoup de temps pour un exemple où la libération d'un verrou avant d'appeler l'équivalent pthread de notify_one()
performances améliorées de manière mesurable.
Gardez à l'esprit que l' lock()
appel dans la while
boucle est nécessaire à un moment donné, car le verrou doit être maintenu pendant la while (!done)
vérification de l'état de la boucle. Mais il n'a pas besoin d'être mis en attente pour l'appel notify_one()
.
2016-02-27 : Grande mise à jour pour répondre à certaines questions dans les commentaires sur la question de savoir s'il y a une condition de concurrence est que le verrou n'aide pas pour l' notify_one()
appel. Je sais que cette mise à jour est en retard car la question a été posée il y a presque deux ans, mais j'aimerais répondre à la question de @ Cookie sur une éventuelle condition de concurrence 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'objet qui indique en fait si le consommateur a ou non un «travail» à faire. Il condition_variable
s'agit simplement d'un mécanisme permettant au consommateur d'attendre efficacement un changement i
.
Le producteur doit maintenir le verrou lors de la mise à jour i
, et le consommateur doit maintenir le verrou pendant la vérification i
et l'appel condition_variable::wait()
(s'il doit attendre du tout). 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 et attente. Puisque la section critique est conservée lorsque le producteur met à jour i
et lorsque le consommateur vérifie et attend i
, il n'y a aucune possibilité i
de changer entre le moment où le consommateur vérifie i
et le moment où 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 0 alors le consommateur appelle cv.wait()
, alors i
sera toujours 0 quand la wait(lock)
partie de l'implémentation est appelée - l'utilisation correcte des verrous garantit cela. Dans ce cas, le producteur n'a pas la possibilité d'appeler le condition_variable::notify_one()
dans sa while
boucle tant que le consommateur n'a pas appelé cv.wait(lk, []{return i == 1;})
(et que l' wait()
appel a fait tout ce qu'il doit faire pour `` attraper '' correctement une notification - wait()
il ne libérera pas le verrou tant qu'il ne l'aura pas 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 wait(lock)
partie de l'implémentation ne sera jamais appelée car le while (!pred())
test provoquera la fin de la boucle interne. Dans cette situation, peu importe quand l'appel à notify_one () se produit - le consommateur ne bloquera pas.
L'exemple ici présente la complexité supplémentaire d'utiliser la done
variable pour signaler au fil producteur que le consommateur a reconnu cela i == 1
, mais je ne pense pas que cela change du tout l'analyse car tout l'accès à done
(pour la lecture et la modification ) se font dans les mêmes sections critiques qui impliquent i
et le condition_variable
.
Si vous regardez la question que @ EH9 a souligné, Sync est peu fiable en utilisant std :: atomique et std :: condition_variable , vous ne voir une condition de course. Cependant, 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 de l'exécution d'une vérification et d'une attente.
Dans cet exemple, le code ressemble à ceci:
if (--f->counter == 0)
f->resume.notify_all();
else
{
unique_lock<mutex> lock(f->resume_mutex);
f->resume.wait(lock);
}
Vous remarquerez que l' wait()
at # 3 est exécuté en maintenant f->resume_mutex
. Mais la vérification de la nécessité ou non de la wait()
nécessité à l'étape 1 n'est pas effectuée tout en maintenant ce verrou (beaucoup moins continuellement pour la vérification et l'attente), ce qui est une exigence pour une utilisation correcte des variables de condition). Je crois que la personne qui a le problème avec cet extrait de code a pensé que depuis, f->counter
c'était un std::atomic
type qui 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ù f->counter
est vérifié (étape # 1) et le moment où le wait()
est appelé (étape # 3).
Cette race n'existe pas dans l'exemple de cette question.
wait morphing
optimisation) Règle de base expliquée dans ce lien: notifier avec verrou est mieux dans les situations avec plus de 2 threads pour des résultats plus prévisibles.the_condition_variable.wait(lock);
. S'il n'y a pas de verrou nécessaire pour synchroniser le producteur et le consommateur (disons que le sous-jacent est une file d'attente spsc sans verrou), alors ce verrou ne sert à rien si le producteur ne le verrouille pas. Très bien pour moi. Mais n'y a-t-il pas un risque pour une course rare? Si le producteur ne tient pas le verrou, ne pourrait-il pas appeler notify_one alors que le consommateur est juste avant l'attente? Ensuite, le consommateur attend et ne se réveille pas ...std::cout << "Waiting... \n";
que le producteur le faitcv.notify_one();
, puis l'appel de réveil disparaît ... Ou est-ce que je manque quelque chose ici?Situation
En utilisant vc10 et Boost 1.56, j'ai implémenté une file d'attente simultanée à peu près comme le suggère cet article de blog . L'auteur déverrouille le mutex pour minimiser les conflits, c'est-à-dire qu'il
notify_one()
est appelé avec le mutex déverrouillé: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 }
Le déverrouillage du mutex est accompagné d'un exemple dans la documentation Boost :
void prepare_data_for_processing() { retrieve_data(); prepare_data(); { boost::lock_guard<boost::mutex> lock(mut); data_ready=true; } cond.notify_one(); }
Problème
Pourtant, cela a conduit au comportement erratique suivant:
notify_one()
n'a pas encore été appelécond_.wait()
peut encore être interrompu viaboost::thread::interrupt()
notify_one()
été appelé pour la première fois descond_.wait()
impasses; l'attente ne peut pas être terminée parboost::thread::interrupt()
ouboost::condition_variable::notify_*()
plus.Solution
La suppression de la ligne a
mlock.unlock()
fait fonctionner le code comme prévu (les notifications et les interruptions mettent fin à l'attente). Notez qu'ilnotify_one()
est appelé avec le mutex toujours verrouillé, il est déverrouillé juste après en quittant la lunette:void push(const T& item) { std::lock_guard<std::mutex> 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 l'appel
boost::condition_variable::notify_one()
, bien que les deux méthodes semblent correctes.la source
Comme d'autres l'ont souligné, vous n'avez pas besoin de maintenir le verrou lors de l'appel
notify_one()
, en termes de conditions de concurrence et de problèmes liés aux threads. Cependant, dans certains cas, il peut être nécessaire de maintenir le verrou pour empêcher lacondition_variable
destruction de l 'avant l'notify_one()
appel. Prenons l'exemple suivant: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) }
Supposons qu'il y ait un changement de contexte vers le thread nouvellement créé
t
après sa création, mais avant de commencer à attendre la variable de condition (quelque part entre (5) et (6)). Le threadt
acquiert le verrou (1), définit la variable de prédicat (2) puis libère le verrou (3). Supposons qu'il y ait un autre changement de contexte juste à ce stade avant quenotify_one()
(4) ne soit exécuté. Le thread principal acquiert le verrou (6) et exécute la ligne (7), à quel point le prédicat revienttrue
et il n'y a aucune raison d'attendre, il libère donc le verrou et continue.foo
renvoie (8) et les variables dans sa portée (y compris (4), à quel point est déjà détruit!cv
) sont détruites. Avant que le threadt
puisse rejoindre le thread principal (9), il doit terminer son exécution, donc il continue là où il s'est arrêté pour s'exécutercv.notify_one()
cv
La solution possible dans ce cas est de continuer à maintenir le verrou lors de l'appel
notify_one
(c'est-à-dire supprimer la portée se terminant à la ligne (3)). Ce faisant, nous nous assurons que lest
appels de threadnotify_one
avantcv.wait
peuvent vérifier la variable de prédicat nouvellement définie et continuer, car il faudrait acquérir le verrou, quit
est actuellement en attente, pour effectuer la vérification. Donc, nous nous assurons que cecv
n'est pas accessible par threadt
après lesfoo
retours.Pour résumer, le problème dans ce cas précis ne concerne pas vraiment le threading, mais la durée de vie des variables capturées par référence.
cv
est capturé par référence via le threadt
, vous devez donc vous assurer qu'ilcv
reste actif pendant toute la durée de l'exécution du thread. Les autres exemples présentés ici ne souffrent pas de ce problème, carcondition_variable
et lesmutex
objets sont définis dans la portée globale, par conséquent, ils sont garantis pour être maintenus en vie jusqu'à ce que le programme se termine.la source
@Michael Burr a raison.
condition_variable::notify_one
ne nécessite pas de verrou sur la variable. Rien ne vous empêche 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 lesignals
thread modifie la variable, il doit s'assurer qu'aucun autre thread n'y accède pendant ce temps.Les verrous sont utilisés pour toute situation nécessitant une synchronisation , je ne pense pas que nous puissions le déclarer de manière plus générale.
la source
wait
fonction de variable de condition libère le verrou à l'intérieur de l'appel et ne revient qu'après avoir réacquis le verrou. après quel point vous pouvez vérifier votre état en toute sécurité parce que vous avez acquis les "droits de lecture" disons. si ce n'est toujours pas ce que vous attendez, vous revenez àwait
. c'est le modèle. btw, cet exemple ne le respecte PAS.Dans certains cas, lorsque le cv peut être occupé (verrouillé) par d'autres threads. Vous devez obtenir le verrou et le libérer avant de notifier _ * ().
Sinon, la notification _ * () peut ne pas être exécutée du tout.
la source
Ajouter simplement cette réponse parce que je pense que la réponse acceptée pourrait être trompeuse. Dans tous les cas, vous devrez verrouiller le mutex, avant d'appeler notifier_one () quelque part pour que votre code soit thread-safe, bien que vous puissiez le déverrouiller à nouveau avant d'appeler notifier _ * ().
Pour clarifier, vous DEVEZ prendre le verrou avant d'entrer wait (lk) car wait () déverrouille lk et ce serait un comportement indéfini si le verrou n'était pas verrouillé. Ce n'est pas le cas avec notify_one (), mais vous devez vous assurer que vous n'appelerez pas notify _ * () avant d'entrer wait () et que cet appel déverrouille le mutex; ce qui ne peut évidemment être fait qu'en verrouillant ce même mutex avant d'appeler notify _ * ().
Par exemple, considérons le cas suivant:
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); }
Attention : ce code contient un bug.
L'idée est la suivante: les threads appellent start () et stop () par paires, mais seulement tant que start () a renvoyé true. Par exemple:
if (start()) { // Do stuff stop(); }
Un (autre) thread à un moment donné appellera cancel () et après son retour de cancel (), il détruira les objets nécessaires à 'Do stuff'. Cependant, cancel () est censé ne pas retourner tant qu'il y a des threads entre start () et stop (), et une fois que cancel () a exécuté sa première ligne, start () retournera toujours false, donc aucun nouveau thread n'entrera dans le champ 'Do zone de trucs.
Fonctionne bien?
Le raisonnement est le suivant:
1) Si un thread exécute avec succès la première ligne de start () (et retournera donc true) alors aucun thread n'a encore exécuté la première ligne de cancel () (nous supposons que le nombre total de threads est bien inférieur à 1000 par le façon).
2) De plus, alors qu'un thread a exécuté avec succès la première ligne de start (), mais pas encore la première ligne de stop (), il est impossible qu'un thread exécute avec succès la première ligne de cancel () (notez qu'un seul thread appelle jamais cancel ()): la valeur renvoyée par fetch_sub (1000) sera supérieure à 0.
3) Une fois qu'un thread a exécuté la première ligne de cancel (), la première ligne de start () retournera toujours false et un thread appelant start () n'entrera plus dans la zone 'Do stuff'.
4) Le nombre d'appels à start () et stop () est toujours équilibré, donc après que la première ligne de cancel () soit exécutée sans succès, il y aura toujours un moment où un (dernier) appel à stop () provoque le décompte pour atteindre -1000 et donc notify_one () à appeler. Notez que cela ne peut se produire que lorsque la première ligne d'annulation a entraîné la chute de ce thread.
À part un problème de famine où tant de threads appellent start () / stop () que count n'atteint jamais -1000 et cancel () ne retourne jamais, ce que l'on pourrait accepter comme "improbable et ne durera jamais longtemps", il y a un autre bogue:
Il est possible qu'il y ait un thread dans la zone 'Do stuff', disons qu'il appelle simplement stop (); à ce moment, un thread exécute la première ligne de cancel () en lisant la valeur 1 avec fetch_sub (1000) et en passant. Mais avant de prendre le mutex et / ou de faire l'appel à wait (lk), le premier thread exécute la première ligne de stop (), lit -999 et appelle cv.notify_one ()!
Ensuite, cet appel à notify_one () est fait AVANT que nous attendions () - ing sur la variable de condition! Et le programme serait indéfiniment impasse.
Pour cette raison, nous ne devrions pas pouvoir appeler notify_one () tant que nous n'avons pas appelé wait (). Notez que la puissance d'une variable de condition réside dans le fait qu'elle est capable de déverrouiller atomiquement le mutex, de vérifier si un appel à notify_one () s'est produit et de s'endormir ou non. Vous ne pouvez pas tromper, mais vous ne le besoin de garder le mutex verrouillé chaque fois que vous apportez des modifications à des variables qui pourraient changer la condition de false à true et garder verrouillé tout en appelant notify_one () en raison des conditions de course comme décrit ici.
Dans cet exemple, il n'y a cependant aucune condition. Pourquoi n'ai-je pas utilisé comme condition «count == -1000»? Parce que ce n'est pas du tout intéressant ici: dès que -1000 est atteint, nous sommes sûrs qu'aucun nouveau thread n'entrera dans la zone 'Do stuff'. De plus, les threads peuvent toujours appeler start () et incrémenteront le nombre (jusqu'à -999 et -998, etc.) mais cela ne nous intéresse pas. La seule chose qui compte, c'est que -1000 a été atteint - pour que nous sachions avec certitude qu'il n'y a plus de threads dans la zone 'Do stuff'. Nous sommes sûrs que c'est le cas lors de l'appel de notify_one (), mais comment s'assurer que nous n'appelons pas notify_one () avant que cancel () verrouille son mutex? Le simple fait de verrouiller cancel_mutex juste avant notifier_one () ne va pas aider bien sûr.
Le problème est que, malgré que nous ne sommes pas en attente d'une condition, il reste est une condition, et nous devons verrouiller le mutex
1) avant que cette condition ne soit atteinte 2) avant d'appeler notify_one.
Le bon code devient donc:
void stop() { if (count.fetch_sub(1) == -999) // Reached -1000 ? { cancel_mutex.lock(); cancel_mutex.unlock(); cv.notify_one(); } }
[... même départ () ...]
void cancel() { std::unique_lock<std::mutex> lk(cancel_mutex); if (count.fetch_sub(1000) == 0) return; cancel_cv.wait(lk); }
Bien sûr, ce n'est qu'un exemple, mais d'autres cas se ressemblent beaucoup; dans presque tous les cas où vous utilisez une variable conditionnelle, vous aurez besoin de verrouiller ce mutex (peu de temps) avant d'appeler notify_one (), sinon il est possible que vous l'appeliez avant d'appeler wait ().
Notez que j'ai déverrouillé le mutex avant d'appeler notify_one () dans ce cas, car sinon il y a une (petite) chance que l'appel à notify_one () réveille le thread en attendant la variable de condition qui essaiera alors de prendre le mutex et bloc, avant de relâcher le mutex. C'est juste un peu plus lent que nécessaire.
Cet exemple était un peu spécial en ce que la ligne qui modifie la condition est exécutée par le même thread qui appelle wait ().
Plus courant est le cas où un thread attend simplement qu'une condition devienne vraie et un autre thread prend le verrou avant de changer les variables impliquées dans cette condition (la faisant éventuellement devenir vraie). Dans ce cas, le mutex est verrouillé immédiatement avant (et après) que la condition ne devienne vraie - il est donc tout à fait normal de déverrouiller simplement le mutex avant d'appeler notify _ * () dans ce cas.
la source