97 votes

Dois-je acquérir un verrou avant d'appeler condition_variable.notify_one() ?

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 trouve pas, c'est si je dois également acquérir un verrou unique avant d'appeler notify_one() o notify_all() .

Exemples sur cppreference.com sont contradictoires. Par exemple, le notify_une page 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, la serrure n'est pas acquise pour la première notify_one() mais il est acquis pour la deuxième notify_one() . En regardant d'autres pages avec des exemples, je vois différentes choses, la plupart n'acquérant pas la serrure.

  • Est-ce que je peux choisir moi-même de verrouiller le mutex avant d'appeler notify_one() et pourquoi je choisirais de la fermer ?
  • Dans l'exemple donné, pourquoi n'y a-t-il pas de verrouillage pour le premier notify_one() mais il y en a pour les appels ultérieurs. Cet exemple est-il erroné ou y a-t-il une justification ?

86voto

Michael Burr Points 181287

Il n'est pas nécessaire de tenir une serrure lors de l'appel. condition_variable::notify_one() mais ce n'est pas une erreur dans le sens où il s'agit toujours d'un comportement bien défini et non d'une erreur.

Cependant, il pourrait s'agir d'une "pessimisation" puisque tout thread en attente rendu exécutable (s'il y en a un) 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 détenir le verrou associé à une variable de condition pendant l'appel à notify_one() o notify_all() . Voir Pthread Mutex : pthread_mutex_unlock() consomme beaucoup de temps pour un exemple où libérer un verrou avant d'appeler l'équivalent pthread de notify_one() a amélioré les performances de façon mesurable.

N'oubliez pas que le lock() dans l'appel while est nécessaire à un moment donné, parce que le verrou doit être maintenu pendant la boucle while (!done) vérification de l'état de la boucle. Mais il n'a pas besoin d'être maintenu pour que l'appel à notify_one() .


2016-02-27 : Importante mise à jour pour répondre à certaines questions dans les commentaires concernant l'existence d'une condition de course si le verrou n'est pas maintenu pour le processus d'exécution de l'opération. notify_one() appeler. Je sais que cette mise à jour est tardive car la question a été posée il y a presque deux ans, mais j'aimerais répondre à la question de @Cookie concernant une éventuelle condition de course 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 réellement si le consommateur a ou non du "travail" à faire. Le site condition_variable est juste un mécanisme pour permettre au consommateur d'attendre efficacement un changement de i .

Le producteur doit conserver le verrou lors de la mise à jour i et le consommateur doit conserver le verrou pendant la vérification. i et en appelant condition_variable::wait() (si tant est qu'il doive attendre). Dans ce cas, l'essentiel est que il doit s'agir de la même instance qui détient le verrou (souvent appelée section critique) lorsque le consommateur effectue cette vérification et cette attente. Puisque la section critique est maintenue quand le producteur met à jour i et lorsque le consommateur vérifie et attend sur i il n'y a pas de possibilité de i pour changer entre le moment où le consommateur vérifie i et lorsqu'il appelle condition_variable::wait() . C'est la clé d'une bonne utilisation des variables de condition.

La norme C++ indique que condition_variable::wait() se comporte comme suit lorsqu'elle est appelée avec un prédicat (comme dans ce cas) :

while (!pred())
    wait(lock);

Deux situations peuvent se produire lorsque le consommateur effectue un contrôle i :

  • si i est égal à 0, le consommateur appelle cv.wait() entonces i sera toujours égal à 0 lorsque la wait(lock) La partie de l'implémentation est appelée - l'utilisation correcte des verrous le garantit. Dans ce cas, le producteur n'a pas l'opportunité d'appeler la fonction condition_variable::notify_one() dans son while jusqu'à ce que le consommateur ait appelé cv.wait(lk, []{return i == 1;}) (et le wait() L'appel a fait tout ce qu'il devait faire pour "attraper" correctement une notification. wait() ne débloquera pas la serrure tant qu'il n'aura pas fait cela). Ainsi, dans ce cas, le consommateur ne peut pas manquer la notification.

  • si i est déjà 1 lorsque le consommateur appelle cv.wait() le wait(lock) de l'implémentation ne sera jamais appelée parce que l'option while (!pred()) provoquera la fin de la boucle interne. Dans cette situation, le moment où l'appel à notify_one() se produit n'a pas d'importance - le consommateur ne bloquera pas.

L'exemple présenté ici présente la complexité supplémentaire de l'utilisation de la fonction done pour signaler au thread du producteur que le consommateur a reconnu que i == 1 mais je ne pense pas que cela change l'analyse du tout au tout car tous les accès à l'information de l'entreprise sont des accès à l'information. done (tant pour la lecture que pour la modification) se font alors que dans les mêmes sections critiques qui impliquent i et le condition_variable .

Si vous regardez la question que @eh9 a pointée, Sync n'est pas fiable en utilisant std::atomic et std::condition_variable vous sera voir une condition de course. Cependant, le code affiché dans cette question viole l'une des règles fondamentales de l'utilisation d'une variable de condition : Il ne contient pas une seule section critique lorsqu'il effectue un check-and-wait.

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<mutex> lock(f->resume_mutex);
    f->resume.wait(lock);   // (3)
}

Vous remarquerez que le wait() au point 3 est effectué en maintenant f->resume_mutex . Mais la vérification pour savoir si oui ou non le wait() est nécessaire à l'étape 1 est pas tout en maintenant ce verrou (et encore moins de manière continue pour le check-and-wait), ce qui est une condition pour une utilisation correcte des variables de condition). Je pense que la personne qui a le problème avec cet extrait de code a pensé que puisque f->counter était un std::atomic type, cela répondrait à l'exigence. Cependant, l'atomicité fournie par std::atomic ne s'étend pas à l'appel ultérieur à f->resume.wait(lock) . Dans cet exemple, il y a une course entre le moment où f->counter est vérifié (étape #1) et lorsque le wait() est appelé (étape n°3).

Cette race n'existe pas dans l'exemple de cette question.

11voto

cantunca Points 216

Comme d'autres l'ont souligné, il n'est pas nécessaire de tenir la serrure lorsque vous appelez notify_one() en termes de conditions de concurrence et de problèmes liés à l'exécution des tâches. Cependant, dans certains cas, il peut être nécessaire de conserver le verrou pour empêcher l'exécution de la tâche. condition_variable de se faire détruire avant notify_one() s'appelle. 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 fil nouvellement créé. t après l'avoir créée mais avant de commencer à attendre la variable de condition (quelque part entre (5) et (6)). Le fil t acquiert le verrou (1), définit la variable prédicat (2) puis libère le verrou (3). Supposons qu'il y ait un autre changement de contexte juste à ce moment-là avant que notify_one() (4) est exécuté. Le thread principal acquiert le verrou (6) et exécute la ligne (7), à laquelle le prédicat retourne true et il n'y a aucune raison d'attendre, donc il libère le verrou et continue. foo (8) et les variables de son champ d'application (y compris les cv ) sont détruits. Avant que le fil t a pu rejoindre le fil d'exécution principal (9), il doit terminer son exécution, donc il reprend là où il s'est arrêté pour exécuter cv.notify_one() (4), auquel cas cv est déjà détruit !

La solution possible dans ce cas est de conserver le verrou lors de l'appel à notify_one (c'est-à-dire supprimer la portée qui se termine à la ligne (3)). En faisant cela, 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 continuer, puisqu'il lui faudrait acquérir le verrou, ce que l'utilisateur ne peut pas faire. t  est en train de tenir, pour faire la vérification. Donc, nous nous assurons que cv n'est pas accédé par le fil t après foo retours.

En résumé, 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 thread t donc vous devez vous assurer cv reste en vie pendant toute la durée d'exécution du thread. Les autres exemples présentés ici ne souffrent pas de ce problème, car condition_variable y mutex sont définis dans la portée globale, ce qui garantit leur maintien en vie jusqu'à la sortie du programme.

10voto

Matthäus Brandl Points 1486

Situation

En utilisant vc10 et Boost 1.56, j'ai implémenté une file d'attente concurrente, à peu près comme suit cet article de blog suggère. L'auteur déverrouille le mutex pour minimiser la contention, c'est-à-dire, 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 étayé par un exemple dans le fichier Améliorer la documentation :

void prepare_data_for_processing()
{
    retrieve_data();
    prepare_data();
    {
        boost::lock_guard<boost::mutex> lock(mut);
        data_ready=true;
    }
    cond.notify_one();
}

Problema

Pourtant, cela a conduit au comportement erratique suivant :

  • tandis que notify_one() a pas n'a pas encore été appelé cond_.wait() peut encore être interrompu par boost::thread::interrupt()
  • une fois notify_one() a été appelé pour la première fois cond_.wait() les blocages ; l'attente ne peut pas être terminée en boost::thread::interrupt() o boost::condition_variable::notify_*() plus.

Solution

Suppression de la ligne mlock.unlock() a fait fonctionner le code comme prévu (les notifications et les interruptions mettent fin à l'attente). Notez que notify_one() est appelé avec le mutex encore verrouillé, il est déverrouillé juste après en quittant la portée :

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 particulière du thread, le mutex ne doit pas être déverrouillé avant d'appeler boost::condition_variable::notify_one() bien que les deux manières semblent correctes.

2voto

Carlo Wood Points 1275

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.

1voto

didierc Points 8128

@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 verrouillage est motivé par l'utilisation simultanée de la variable i . Parce que le signals filetage modifie la variable, il doit s'assurer qu'aucun autre thread n'y accède pendant ce temps.

Les serrures sont utilisées pour toute situation nécessitant synchronisation Je ne pense pas que nous puissions le dire de manière plus générale.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X