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)
cv.notify_one();
}
bool start()
{
if (count.fetch_add(1) >= 0)
return true;
stop();
return false;
}
void cancel()
{
if (count.fetch_sub(1000) == 0)
return;
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())
{
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)
{
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.
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.