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

90

Je suis un peu confus quant à l'utilisation de std::condition_variable. Je comprends que je dois créer un unique_locksur un mutexavant d'appeler condition_variable.wait(). Ce que je ne trouve pas, c'est si je devrais également acquérir un verrou unique avant d'appeler notify_one()ou notify_all().

Les exemples sur cppreference.com sont contradictoires. 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 le premier notify_one(), mais pour le second notify_one(). En regardant d'autres pages avec des exemples, je vois des choses différentes, la plupart du temps n'acquérant pas le verrou.

  • Puis-je choisir moi-même de verrouiller le mutex avant d'appeler notify_one(), et pourquoi choisirais-je de le verrouiller?
  • Dans l'exemple donné, pourquoi n'y a-t-il pas de verrou pour le premier notify_one(), mais il y en a pour les appels suivants. Cet exemple est-il faux ou y a-t-il une justification?
Peter Smit
la source

Réponses:

77

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 whileboucle 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_variables'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 iet 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 iet lorsque le consommateur vérifie et attend i, il n'y a aucune possibilité ide changer entre le moment où le consommateur vérifie iet 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 iest 0 alors le consommateur appelle cv.wait(), alors isera 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 whileboucle 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 iest 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 donevariable 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 iet 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)      // (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 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->counterc'était un std::atomictype qui répondrait à l'exigence. Cependant, l'atomicité fournie par std::atomicne s'étend pas à l'appel suivant à f->resume.wait(lock). Dans cet exemple, il y a une course entre le moment où f->counterest 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.

Michael Burr
la source
2
il a des implications plus profondes: domaigne.com/blog/computing/ ... Notamment, le problème de pthread que vous mentionnez devrait être résolu soit par une version plus récente, soit par une version construite avec les bons indicateurs. (pour activer l' wait morphingoptimisation) 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.
v.oddou
6
@Michael: À ma connaissance, le consommateur doit éventuellement appeler 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 ...
Cookie
1
Par exemple, dites dans le code ci-dessus où se trouve le consommateur pendant std::cout << "Waiting... \n";que le producteur le fait cv.notify_one();, puis l'appel de réveil disparaît ... Ou est-ce que je manque quelque chose ici?
Cookie du
1
@Biscuit. Oui, il y a une condition de course là-bas. Voir stackoverflow.com/questions/20982270/…
eh9
1
@ eh9: Merde, je viens de trouver la cause d'un bug qui gèle mon code de temps en temps grâce à ton commentaire. C'était dû à ce cas précis de condition de course. Le déverrouillage du mutex après la notification a complètement résolu le problème ... Merci beaucoup!
galinette
10

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:

  • tandis que notify_one()n'a pas encore été appelé cond_.wait()peut encore être interrompu viaboost::thread::interrupt()
  • une fois a notify_one()été appelé pour la première fois des cond_.wait()impasses; l'attente ne peut pas être terminée par boost::thread::interrupt()ou boost::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'il notify_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.

Matthäus Brandl
la source
Avez-vous signalé ce problème à Boost.Thread? Je ne trouve pas de tâche similaire ici svn.boost.org/trac/boost/…
magras
@magras Malheureusement, je ne l'ai pas fait, je ne sais pas pourquoi je n'ai pas envisagé cela. Et malheureusement je ne parviens pas à reproduire cette erreur en utilisant la file d'attente mentionnée.
Matthäus Brandl
Je ne suis pas sûr de voir à quel point un réveil précoce pourrait provoquer une impasse. Plus précisément, si vous sortez de cond_.wait () dans pop () après que push () libère le mutex de la file d'attente mais avant que notify_one () ne soit appelé - Pop () devrait voir la file d'attente non vide et consommer la nouvelle entrée plutôt que attendre. si vous sortez de cond_.wait () pendant que push () met à jour la file d'attente, le verrou doit être maintenu par push (), donc pop () devrait bloquer en attendant que le verrou soit libéré. Tout autre réveil précoce maintiendrait le verrou, empêchant push () de modifier la file d'attente avant que pop () appelle le prochain wait (). Qu'est-ce que j'ai raté?
Kevin
4

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 la condition_variabledestruction 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éé taprès sa création, mais avant de commencer à attendre la variable de condition (quelque part entre (5) et (6)). Le thread tacquiert 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 que notify_one()(4) ne soit exécuté. Le thread principal acquiert le verrou (6) et exécute la ligne (7), à quel point le prédicat revient trueet il n'y a aucune raison d'attendre, il libère donc le verrou et continue. foorenvoie (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 thread tpuisse 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 les tappels de thread notify_oneavant cv.waitpeuvent vérifier la variable de prédicat nouvellement définie et continuer, car il faudrait acquérir le verrou, qui t est actuellement en attente, pour effectuer la vérification. Donc, nous nous assurons que ce cvn'est pas accessible par thread taprès les fooretours.

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. cvest capturé par référence via le thread t, vous devez donc vous assurer qu'il cvreste 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, car condition_variableet les mutexobjets 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.

cantunca
la source
1

@Michael Burr a raison. condition_variable::notify_onene 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 le signalsthread 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.

didierc
la source
bien sûr, mais en plus de cela, ils doivent également être utilisés en conjonction avec des variables de condition afin que l'ensemble du modèle fonctionne réellement. notamment la waitfonction 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.
v.oddou
1

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.

Fan Jing
la source
1

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.

Carlo Wood
la source