web-dev-qa-db-fra.com

Dois-je obtenir 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 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.

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

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.

52
Michael Burr

Situation

À 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();
}

Problème

Cela a néanmoins conduit au comportement erratique suivant:

  • alors que notify_one() a pas déjà été appelé cond_.wait() peut toujours être interrompu via boost::thread::interrupt()
  • une fois que 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_*().

Solution

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.

8
Matthäus Brandl

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.

1
Fan Jing

@ 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. 

1
didierc

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.

0
Can Tunca