J'ajoute simplement cette réponse car je pense que la réponse acceptée peut être trompeuse. Dans tous les cas, vous devrez verrouiller le mutex avant d'appeler notify_one(). quelque part pour que votre code soit thread-safe, bien que vous puissiez le déverrouiller à nouveau avant d'appeler notify_*().
Pour clarifier, vous DEVEZ prendre le verrou avant d'entrer dans 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'appellerez pas notify_*() avant d'entrer dans wait(). y en faisant en sorte 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);
}
Avertissement : ce code contient un bug.
L'idée est la suivante : les threads appellent start() et stop() par paires, mais seulement tant que start() renvoie vrai. Par exemple :
if (start())
{
// Do stuff
stop();
}
À un moment donné, un (autre) thread appellera cancel() et, après le retour de cancel(), détruira les objets nécessaires pour "faire des choses". Cependant, cancel() est censé ne pas revenir 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 la zone 'Do stuff'.
Ça marche, non ?
Le raisonnement est le suivant :
1) Si un thread exécute avec succès la première ligne de start() (et renvoie donc true), alors aucun thread n'a encore exécuté la première ligne de cancel() (nous supposons d'ailleurs que le nombre total de threads est bien inférieur à 1000).
2) De plus, si un thread a réussi à exécuter la première ligne de start(), mais pas encore la première ligne de stop(), il est impossible qu'un thread quelconque réussisse à exécuter la première ligne de cancel() (notez qu'un seul thread appelle 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() ait été exécutée sans succès, il y aura toujours un moment où un (dernier) appel à stop() fera que le compte atteigne -1000 et donc que notify_one() soit appelé. Notez que cela ne peut se produire que si la première ligne de cancel() a entraîné la chute de ce thread.
En dehors d'un problème de famine où tant de threads appellent start()/stop() que le compte n'atteint jamais -1000 et que cancel() ne revient jamais, ce que l'on pourrait accepter comme "improbable et qui ne dure jamais longtemps", il y a un autre bug :
Il est possible qu'il y ait un thread à l'intérieur de la zone 'Do stuff', disons qu'il appelle stop() ; à ce moment-là, un thread exécute la première ligne de cancel(), lit la valeur 1 avec fetch_sub(1000) et passe à travers. 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() !
Alors cet appel à notify_one() est fait AVANT que nous attendions la variable de condition ! Et le programme se bloquerait indéfiniment.
Pour cette raison, nous ne devrions pas être en mesure d'appeler notify_one() jusqu'à nous avons appelé wait(). Notez que la puissance d'une variable de condition réside ici 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 la tromper, mais vous faire vous devez garder le mutex verrouillé chaque fois que vous apportez des modifications aux variables qui pourraient faire passer la condition de faux à vrai et garder il s'est verrouillé en appelant notify_one() à cause de conditions de course comme celles décrites 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 fil n'entrera dans la zone 'Do stuff'. De plus, les threads peuvent toujours appeler start() et incrémenter le compte (jusqu'à -999 et -998 etc.) mais nous ne nous en soucions pas. La seule chose qui compte est que -1000 a été atteint - de sorte que nous sommes sûrs qu'il n'y a plus de threads dans la zone "Do stuff". Nous sommes sûrs que c'est le cas lorsque notify_one() est appelé, mais comment s'assurer que nous n'appelons pas notify_one() avant que cancel() ait verrouillé son mutex ? Verrouiller le mutex de cancel juste avant notify_one() ne va pas aider bien sûr.
Le problème est que, malgré le fait que nous n'attendons pas de condition, il y a toujours est une condition, et nous devons verrouiller le mutex
1) avant que cette condition ne soit atteinte 2) avant d'appeler notify_one.
Le code correct devient donc :
void stop()
{
if (count.fetch_sub(1) == -999) // Reached -1000 ?
{
cancel_mutex.lock();
cancel_mutex.unlock();
cv.notify_one();
}
}
[...même start()...]
void cancel()
{
std::unique_lock<std::mutex> lk(cancel_mutex);
if (count.fetch_sub(1000) == 0)
return;
cancel_cv.wait(lk);
}
Bien sûr, il ne s'agit que d'un exemple, mais les autres cas sont très similaires ; dans presque tous les cas où vous utilisez une variable conditionnelle, vous devrez besoin de pour avoir ce mutex verrouillé (peu de temps) avant d'appeler notify_one(), ou bien 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 attente de la variable de condition qui va alors essayer de prendre le mutex et bloquer, avant que nous libérions à nouveau le mutex. C'est juste un peu plus lent que nécessaire.
Cet exemple est un peu spécial dans la mesure où la ligne qui modifie la condition est exécutée par le même thread qui appelle wait().
Le cas le plus courant est celui où un thread attend simplement qu'une condition devienne vraie et où un autre thread prend le verrou avant de modifier les variables impliquées dans cette condition (ce qui pourrait la rendre vraie). Dans ce cas, le mutex est verrouillé immédiatement avant (et après) que la condition devienne vraie - il est donc tout à fait correct de déverrouiller le mutex avant d'appeler notify_*() dans ce cas.