Je suis un peu confus quant à l'utilisation de std::condition_variable
. Je comprends que je dois créer un unique_lock
sur une mutex
avant d’appeler condition_variable.wait()
. Ce que je ne peux pas trouver, c'est si je dois également 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 <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();
}
Ici, le verrou n'est pas acquis pour la première notify_one()
, mais est acquis pour la deuxième notify_one()
. En regardant d'autres pages avec des exemples, je vois différentes choses, principalement en n'acquérant pas le verrou.
notify_one()
et pourquoi devrais-je choisir de le verrouiller?notify_one()
, mais pour les appels suivants? Cet exemple est-il faux ou y a-t-il une raison?Vous n'avez pas besoin de garder un verrou lorsque vous appelez condition_variable::notify_one()
, mais ce n'est pas une erreur en ce sens que le comportement reste bien défini et qu'il ne s'agit pas d'une erreur.
Cependant, cela pourrait être une "pessimisation" puisque tout le thread en attente qui sera 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 empirique d'éviter de conserver le verrou associé à une variable de condition lors de l'appel de notify_one()
ou notify_all()
. Voir Pthread Mutex: pthread_mutex_unlock () prend beaucoup de temps pour un exemple où libérer un verrou avant d'appeler l'équivalent pthread de notify_one()
améliorait les performances de manière mesurable.
N'oubliez pas 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 de la boucle while (!done)
. Mais il n'est pas nécessaire de le suspendre pour l'appel à notify_one()
.
2016-02-27: Mise à jour importante pour répondre à certaines questions dans les commentaires sur le fait qu'il existe ou non une condition de concurrence critique si le verrou ne permet pas de résoudre l'appel notify_one()
. Je sais que cette mise à jour est en retard car la question a été posée il y a près de deux ans, mais j'aimerais répondre à la question de @ Cookie sur une possible condition de concurrence critique si le producteur (signals()
dans cet exemple) appelle notify_one()
juste avant le consommateur (waits()
dans cette exemple) peut appeler wait()
.
La clé est ce qui arrive à i
- c'est l'objet qui indique si le consommateur doit ou non "travailler". Le condition_variable
est simplement un mécanisme permettant au consommateur d’attendre efficacement un changement de i
.
Le producteur doit conserver le verrou lors de la mise à jour de i
et le consommateur doit le conserver tout en vérifiant i
et en appelant condition_variable::wait()
(s’il doit attendre du tout). Dans ce cas, la clé est que il doit s'agir de la même instance de verrouillage (souvent appelée section critique) lorsque le consommateur effectue cette vérification et cette attente. Étant donné que 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é pour 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.
La norme 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);
Le consommateur vérifie i
dans deux situations différentes:
si i
est 0, le consommateur appelle cv.wait()
, alors i
sera toujours 0 lorsque la partie wait(lock)
de l'implémentation sera 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
tant que le consommateur n'a pas appelé cv.wait(lk, []{return i == 1;})
(et l'appel wait()
n'a rien fait de mal à "attraper" une notification - wait()
ne lâche pas le verrou. jusqu'à ce qu'il l'ait 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())
entraînera l'arrêt de la boucle interne. Dans cette situation, peu importe le moment où l'appel à notify_one () se produit: le consommateur ne bloquera pas.
L’exemple présenté 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 modifie l’analyse du tout, car tout l’accès à done
(pour les deux lire et modifier) sont effectuées dans les mêmes sections critiques qui impliquent i
et le condition_variable
.
Si vous regardez la question à laquelle @ eh9 a fait allusion, Sync n'est pas fiable avec std :: atomic et std :: condition_variable , vous (volonté} _ voyez une condition de concurrence critique. Toutefois, le code affiché dans cette question enfreint l'une des règles fondamentales de l'utilisation d'une variable de condition: il ne contient pas une seule section critique lors de l'exécution d'une vérification et de l'attente.
Dans cet exemple, le code ressemble à ceci:
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)
}
Vous remarquerez que la wait()
en 3 est exécutée en maintenant f->resume_mutex
. Mais la vérification de la nécessité ou non de wait()
à l'étape 1 est non effectuée tout en maintenant ce verrou (beaucoup moins continuellement pour la vérification et l'attente), ce qui est indispensable pour une utilisation correcte des 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 satisferait à 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 coché (étape 1) et celui où la wait()
est appelée (étape 3).
Cette race n'existe pas dans l'exemple de cette question.
À l’aide de vc10 et de Boost 1.56, j’ai implémenté une file d’attente concurrente à peu près comme cet article de blog suggère. L’auteur déverrouille le mutex pour minimiser les conflits, c’est-à-dire que 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 s'appuie sur un exemple de 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();
}
Cela a néanmoins conduit au comportement erratique suivant:
notify_one()
a pas déjà été appelé cond_.wait()
peut toujours être interrompu via boost::thread::interrupt()
notify_one()
a été appelé pour la première fois cond_.wait()
à des impasses; l'attente ne peut plus être terminée par boost::thread::interrupt()
ou boost::condition_variable::notify_*()
.En supprimant la ligne mlock.unlock()
, le code a fonctionné comme prévu (les notifications et les interruptions mettent fin à l'attente). Notez que notify_one()
est appelé avec le mutex toujours verrouillé, il est déverrouillé juste après lorsque vous quittez l'oscilloscope:
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 d'appeler boost::condition_variable::notify_one()
, bien que les deux méthodes semblent correctes.
Dans certains cas, lorsque le cv peut être occupé (verrouillé) par d'autres threads. Vous devez le verrouiller et le relâcher avant de notifier _ * ().
Sinon, le notifier _ * () peut ne pas être exécuté du tout.
@ 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 le montre l’exemple.
Dans l'exemple donné, le verrou est motivé par l'utilisation simultanée de la variable i
. Étant donné que le thread signals
modifie la variable, il doit s’assurer qu’aucun autre thread n’y a accès pendant cette période.
Les verrous sont utilisés pour toutes les situations nécessitant synchronisation , je ne pense pas que nous puissions le dire de manière plus générale.
Comme d'autres l'ont fait remarquer, il n'est pas nécessaire de garder le verrou lorsque vous appelez 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 la destruction du condition_variable
avant l'appel de notify_one()
. 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 existe un changement de contexte dans le thread nouvellement créé t
après l'avoir créé mais avant de commencer à attendre la variable de condition (quelque part entre (5) et (6)). Le thread t
acquiert le verrou (1), définit la variable de prédicat (2) puis libère le verrou (3). Supposons qu’il existe un autre commutateur de contexte juste avant l’exécution de notify_one()
(4). Le thread principal acquiert le verrou (6) et exécute la ligne (7), point auquel le prédicat renvoie true
et il n'y a aucune raison d'attendre. Il libère alors le verrou et continue. foo
renvoie (8) et les variables de son étendue (y compris cv
) sont détruites. Avant que le thread t
puisse rejoindre le thread principal (9), il doit terminer son exécution, il continue donc là où il s’était arrêté pour exécuter cv.notify_one()
(4), où cv
est déjà détruit!
La solution possible dans ce cas est de garder le verrou lorsque vous appelez notify_one
(c'est-à-dire, supprimez la portée se terminant par la ligne (3)). Ce faisant, nous nous assurons que le thread t
appelle notify_one
avant cv.wait
peut vérifier la variable de prédicat nouvellement définie et continue, car il lui faudrait acquérir le verrou que t
détient actuellement pour effectuer la vérification. Donc, nous nous assurons que cv
n'est pas accédé par le thread t
après le retour de foo
.
Pour résumer, le problème dans ce cas spécifique 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 thread t
, vous devez donc vous assurer que cv
reste en vie 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, car les objets condition_variable
et mutex
sont définis dans la portée globale et sont donc garantis d'être maintenus en vie jusqu'à la fermeture du programme.