Pourquoi les fonctions de variable de condition de pthreads nécessitent-elles un mutex?


185

Je lis sur pthread.h; les fonctions liées aux variables de condition (comme pthread_cond_wait(3)) nécessitent un mutex comme argument. Pourquoi? Pour autant que je sache, je vais créer un mutex juste pour l'utiliser comme cet argument? Qu'est-ce que ce mutex est censé faire?

Réponses:


197

C'est juste la façon dont les variables de condition sont (ou étaient à l'origine) implémentées.

Le mutex est utilisé pour protéger la variable de condition elle-même . C'est pourquoi vous devez le verrouiller avant d'attendre.

L'attente déverrouillera "atomiquement" le mutex, permettant aux autres d'accéder à la variable de condition (pour la signalisation). Ensuite, lorsque la variable de condition est signalée ou diffusée vers, un ou plusieurs threads de la liste d'attente seront réveillés et le mutex sera à nouveau verrouillé par magie pour ce thread.

Vous voyez généralement l'opération suivante avec des variables de condition, illustrant leur fonctionnement. L'exemple suivant est un thread de travail qui reçoit du travail via un signal à une variable de condition.

thread:
    initialise.
    lock mutex.
    while thread not told to stop working:
        wait on condvar using mutex.
        if work is available to be done:
            do the work.
    unlock mutex.
    clean up.
    exit thread.

Le travail se fait au sein de cette boucle à condition qu'il y en ait de disponible au retour de l'attente. Lorsque le thread a été marqué pour arrêter de faire le travail (généralement par un autre thread définissant la condition de sortie puis coupant la variable de condition pour réveiller ce thread), la boucle se terminera, le mutex sera déverrouillé et ce thread se fermera.

Le code ci-dessus est un modèle à consommateur unique car le mutex reste verrouillé pendant que le travail est en cours. Pour une variante multi-consommateurs, vous pouvez utiliser, à titre d' exemple :

thread:
    initialise.
    lock mutex.
    while thread not told to stop working:
        wait on condvar using mutex.
        if work is available to be done:
            copy work to thread local storage.
            unlock mutex.
            do the work.
            lock mutex.
    unlock mutex.
    clean up.
    exit thread.

ce qui permet à d'autres consommateurs de recevoir du travail pendant que celui-ci travaille.

La variable de condition vous libère du fardeau d'interroger une condition au lieu de permettre à un autre thread de vous avertir lorsque quelque chose doit se produire. Un autre thread peut dire que le thread qui fonctionne est disponible comme suit:

lock mutex.
flag work as available.
signal condition variable.
unlock mutex.

La grande majorité de ce que l'on appelle souvent à tort des réveils parasites était généralement toujours parce que plusieurs threads avaient été signalés dans leur pthread_cond_waitappel (diffusion), on revenait avec le mutex, faisait le travail, puis attendait à nouveau.

Ensuite, le deuxième fil signalé pouvait sortir lorsqu'il n'y avait pas de travail à faire. Vous deviez donc avoir une variable supplémentaire indiquant que le travail devait être fait (cela était intrinsèquement protégé contre le mutex avec la paire condvar / mutex ici - d'autres threads devaient cependant verrouiller le mutex avant de le modifier).

Il était techniquement possible pour un thread de revenir d'une condition d'attente sans être lancé par un autre processus (il s'agit d'un véritable faux réveil) mais, au cours de toutes mes années de travail sur pthreads, à la fois dans le développement / service du code et en tant qu'utilisateur d’entre eux, je n’en ai jamais reçu un seul. Peut-être était-ce simplement parce que HP avait une implémentation décente :-)

Dans tous les cas, le même code qui a traité le cas erroné a également géré les véritables réveils parasites, car l'indicateur de travail disponible ne serait pas défini pour ceux-ci.


3
«faire quelque chose» ne devrait pas être dans la boucle while. Vous voudriez que votre boucle while vérifie simplement la condition, sinon vous pourriez aussi «faire quelque chose» si vous obtenez un faux réveil.
nos

1
non, la gestion des erreurs est en second lieu. Avec pthreads, vous pouvez être réveillé, sans raison apparente (un faux réveil), et sans aucune erreur. Ainsi, vous devez revérifier «certaines conditions» après votre réveil.
nos

2
Peut-être que je ne suis pas assez clair. La boucle n'est pas d'attendre que le travail soit prêt pour que vous puissiez le faire. La boucle est la principale boucle de travail «infinie». Si vous revenez de cond_wait et que l'indicateur de travail est défini, vous effectuez le travail puis bouclez à nouveau. "while some condition" ne sera fausse que si vous voulez que le thread arrête de travailler, à quel point il libère le mutex et se termine très probablement.
paxdiablo

8
@stefaanv "le mutex est toujours pour protéger la variable de condition, il n'y a pas d'autre moyen de la protéger": le mutex ne sert pas à protéger la variable de condition; c'est pour protéger les données de prédicat , mais je pense que vous le savez à la lecture de votre commentaire qui a suivi cette déclaration. Vous pouvez signaler une variable de condition légalement et entièrement prise en charge par les implémentations, après le déverrouillage du mutex enveloppant le prédicat, et en fait, vous soulagerez les conflits en le faisant dans certains cas.
WhozCraig

2
@WhozCraig, +1, oui, le mutex ne sert PAS à protéger la variable de condition.
Arun

62

Une variable de condition est assez limitée si vous ne pouviez signaler qu'une condition, vous devez généralement gérer certaines données liées à la condition qui a été signalée. La signalisation / réveil doit être effectuée de manière atomique pour y parvenir sans introduire de conditions de course, ou être trop complexe

pthreads peut également vous donner, pour des raisons plutôt techniques, un faux réveil . Cela signifie que vous devez vérifier un prédicat, pour être sûr que la condition a réellement été signalée - et distinguer cela d'un faux réveil. La vérification d'une telle condition en ce qui concerne l'attente doit être protégée - une variable de condition a donc besoin d'un moyen d'attendre / de se réveiller atomiquement tout en verrouillant / déverrouillant un mutex protégeant cette condition.

Prenons un exemple simple où vous êtes informé que certaines données sont produites. Peut-être qu'un autre thread a créé les données souhaitées et a défini un pointeur vers ces données.

Imaginez un thread producteur donnant des données à un autre thread consommateur via un pointeur 'some_data'.

while(1) {
    pthread_cond_wait(&cond); //imagine cond_wait did not have a mutex
    char *data = some_data;
    some_data = NULL;
    handle(data);
}

vous auriez naturellement beaucoup de condition de course, que se passerait-il si l'autre thread faisait some_data = new_datajuste après que vous vous soyez réveillé, mais avant vousdata = some_data

Vous ne pouvez pas vraiment créer votre propre mutex pour protéger ce cas non plus .eg

while(1) {

    pthread_cond_wait(&cond); //imagine cond_wait did not have a mutex
    pthread_mutex_lock(&mutex);
    char *data = some_data;
    some_data = NULL;
    pthread_mutex_unlock(&mutex);
    handle(data);
}

Cela ne fonctionnera pas, il y a toujours une chance de condition de course entre le réveil et la prise du mutex. Placer le mutex avant pthread_cond_wait ne vous aide pas, car vous tiendrez maintenant le mutex en attendant - c'est-à-dire que le producteur ne pourra jamais saisir le mutex. (notez que dans ce cas, vous pouvez créer une deuxième variable de condition pour signaler au producteur que vous en avez terminé some_data- bien que cela devienne complexe, surtout si vous voulez de nombreux producteurs / consommateurs.)

Ainsi, vous avez besoin d'un moyen de libérer / récupérer atomiquement le mutex lors de l'attente / du réveil de la condition. C'est ce que font les variables de condition pthread, et voici ce que vous feriez:

while(1) {
    pthread_mutex_lock(&mutex);
    while(some_data == NULL) { // predicate to acccount for spurious wakeups,would also 
                               // make it robust if there were several consumers
       pthread_cond_wait(&cond,&mutex); //atomically lock/unlock mutex
    }

    char *data = some_data;
    some_data = NULL;
    pthread_mutex_unlock(&mutex);
    handle(data);
}

(le producteur devrait naturellement prendre les mêmes précautions, en gardant toujours 'some_data' avec le même mutex, et en s'assurant qu'il n'écrase pas some_data si some_data est actuellement! = NULL)


Ne devrait-il pas while (some_data != NULL)être une boucle do-while pour qu'il attende la variable de condition au moins une fois?
Judge Maygarden

3
Non. Ce que vous attendez vraiment, c'est que 'some_data' soit non nul. S'il n'est pas nul la "première fois", c'est parfait, vous tenez le mutex et pouvez utiliser les données en toute sécurité. Si vous aviez une boucle do / while, vous manqueriez la notification si quelqu'un signalait la variable de condition avant de l'attendre (cela n'a rien à voir avec les événements trouvés sur win32 qui restent signalés jusqu'à ce que quelqu'un les attend)
nos

5
Je viens de trébucher sur cette question et franchement, il est étrange de constater que cette réponse, qui est juste correcte a tellement moins de points que la réponse de paxdiablo qui a des défauts certains (l'atomicité est toujours nécessaire, le mutex n'est nécessaire que pour gérer la condition, pas pour la manipulation ou la notification). Je suppose que c'est comme ça que fonctionne stackoverflow ...
stefaanv

@stefaanv, si vous souhaitez détailler les défauts, en commentaire de ma réponse afin que je les vois en temps opportun, plutôt que des mois plus tard :-), je serai heureux de les corriger. Vos brèves phrases ne me donnent pas vraiment assez de détails pour comprendre ce que vous essayez de dire.
paxdiablo

1
@nos, ça ne devrait pas l' while(some_data != NULL)être while(some_data == NULL)?
Eric Z

30

Les variables de condition POSIX sont sans état. Il est donc de votre responsabilité de maintenir l'état. Étant donné que l'état sera accessible à la fois par les threads qui attendent et les threads qui disent aux autres threads d'arrêter d'attendre, il doit être protégé par un mutex. Si vous pensez pouvoir utiliser des variables de condition sans mutex, vous n'avez pas compris que les variables de condition sont sans état.

Les variables de condition sont construites autour d'une condition. Les threads qui attendent une variable de condition attendent une condition. Les threads qui signalent des variables de condition modifient cette condition. Par exemple, un thread peut attendre l'arrivée de certaines données. Un autre thread peut remarquer que les données sont arrivées. «Les données sont arrivées» est la condition.

Voici l'utilisation classique d'une variable de condition, simplifiée:

while(1)
{
    pthread_mutex_lock(&work_mutex);

    while (work_queue_empty())       // wait for work
       pthread_cond_wait(&work_cv, &work_mutex);

    work = get_work_from_queue();    // get work

    pthread_mutex_unlock(&work_mutex);

    do_work(work);                   // do that work
}

Voyez comment le fil attend le travail. L'œuvre est protégée par un mutex. L'attente libère le mutex afin qu'un autre thread puisse donner du travail à ce thread. Voici comment cela serait signalé:

void AssignWork(WorkItem work)
{
    pthread_mutex_lock(&work_mutex);

    add_work_to_queue(work);           // put work item on queue

    pthread_cond_signal(&work_cv);     // wake worker thread

    pthread_mutex_unlock(&work_mutex);
}

Notez que vous avez besoin du mutex pour protéger la file d'attente de travail. Notez que la variable de condition elle-même n'a aucune idée s'il y a du travail ou non. Autrement dit, une variable de condition doit être associée à une condition, cette condition doit être maintenue par votre code et, comme elle est partagée entre les threads, elle doit être protégée par un mutex.


1
Ou, pour le dire plus concis, le point entier des variables de condition est de fournir une opération atomique "déverrouiller et attendre". Sans un mutex, il n'y aurait rien à débloquer.
David Schwartz

Pourriez-vous expliquer la signification du terme apatride ?
snr

@snr Ils n'ont aucun état. Ils ne sont pas «verrouillés», «signalés» ou «non signalés». Il est donc de votre responsabilité de garder une trace de tout état associé à la variable de condition. Par exemple, si la variable de condition informe un thread quand une file d'attente devient non vide, il doit arriver qu'un thread puisse rendre la file non vide et un autre thread doit savoir quand la file d'attente devient non vide. C'est un état partagé et vous devez le protéger avec un mutex. Vous pouvez utiliser la variable de condition, en association avec cet état partagé protégé par un mutex, comme mécanisme de réveil.
David Schwartz

16

Toutes les fonctions de variable de condition ne nécessitent pas de mutex: seules les opérations en attente le font. Les opérations de signal et de diffusion ne nécessitent pas de mutex. Une variable de condition n'est pas non plus associée en permanence à un mutex spécifique; le mutex externe ne protège pas la variable de condition. Si une variable de condition a un état interne, comme une file d'attente de threads en attente, cela doit être protégé par un verrou interne à l'intérieur de la variable de condition.

Les opérations d'attente rassemblent une variable de condition et un mutex, car:

  • un thread a verrouillé le mutex, évalué une expression sur des variables partagées et l'a trouvé faux, de sorte qu'il doit attendre.
  • le thread doit passer de manière atomique de la possession du mutex à l'attente de la condition.

Pour cette raison, l'opération d'attente prend comme arguments à la fois le mutex et la condition: afin qu'elle puisse gérer le transfert atomique d'un thread de la possession du mutex à l'attente, afin que le thread ne soit pas victime de la condition de course de réveil perdu .

Une condition de course de réveil perdu se produira si un thread abandonne un mutex, puis attend un objet de synchronisation sans état, mais d'une manière qui n'est pas atomique: il existe une fenêtre de temps lorsque le thread n'a plus le verrou, et a pas encore commencé à attendre l'objet. Durant cette fenêtre, un autre thread peut entrer, rendre la condition attendue vraie, signaler la synchronisation sans état puis disparaître. L'objet sans état ne se souvient pas qu'il a été signalé (il est sans état). Ainsi, le thread d'origine se met en veille sur l'objet de synchronisation sans état et ne se réveille pas, même si la condition dont il a besoin est déjà devenue vraie: réveil perdu.

Les fonctions d'attente de variable de condition évitent le réveil perdu en s'assurant que le thread appelant est enregistré pour attraper de manière fiable le réveil avant qu'il n'abandonne le mutex. Cela serait impossible si la fonction d'attente de la variable de condition ne prenait pas le mutex comme argument.


Pourriez-vous indiquer que les opérations de diffusion ne nécessitent pas d'acquérir le mutex? Sur MSVC, la diffusion est ignorée.
xvan

@xvan Le POSIX pthread_cond_broadcastet les pthread_cond_signalopérations (dont traite cette question SO) ne prennent même pas le mutex comme argument; seulement la condition. La spécification POSIX est ici . Le mutex n'est mentionné qu'en référence à ce qui se passe dans les threads en attente lorsqu'ils se réveillent.
Kaz

Pourriez-vous expliquer la signification du terme apatride ?
snr

1
@snr Un objet de synchronisation sans état ne se souvient d'aucun état lié à la signalisation. Lorsqu'il est signalé, si quelque chose l'attend maintenant, il est réveillé, sinon le réveil est oublié. Les variables de condition sont sans état comme ça. L'état nécessaire pour fiabiliser la synchronisation est maintenu par l'application et protégé par le mutex qui est utilisé en conjonction avec les variables de condition, selon une logique correctement écrite.
Kaz

7

Je ne trouve pas les autres réponses aussi concises et lisibles que cette page . Normalement, le code d'attente ressemble à ceci:

mutex.lock()
while(!check())
    condition.wait()
mutex.unlock()

Il y a trois raisons d'envelopper le wait()dans un mutex:

  1. sans un mutex un autre fil pourrait signal()avant le wait()et nous manquerions ce réveil.
  2. check()dépend normalement de la modification d'un autre thread, vous avez donc besoin d'une exclusion mutuelle de toute façon.
  3. pour s'assurer que le thread de priorité la plus élevée se déroule en premier (la file d'attente du mutex permet au planificateur de décider qui va ensuite).

Le troisième point n'est pas toujours une préoccupation - le contexte historique est lié de l'article à cette conversation .

Des réveils parasites sont souvent mentionnés à propos de ce mécanisme (c'est-à-dire que le thread en attente est réveillé sans signal()être appelé). Cependant, ces événements sont gérés par le looped check().


4

Les variables de condition sont associées à un mutex car c'est le seul moyen pour lui d'éviter la course qu'il est censé éviter.

// incorrect usage:
// thread 1:
while (notDone) {
    pthread_mutex_lock(&mutex);
    bool ready = protectedReadyToRunVariable
    pthread_mutex_unlock(&mutex);
    if (ready) {
        doWork();
    } else {
        pthread_cond_wait(&cond1); // invalid syntax: this SHOULD have a mutex
    }
}

// signalling thread
// thread 2:
prepareToRunThread1();
pthread_mutex_lock(&mutex);
   protectedReadyToRuNVariable = true;
pthread_mutex_unlock(&mutex);
pthread_cond_signal(&cond1);

Now, lets look at a particularly nasty interleaving of these operations

pthread_mutex_lock(&mutex);
bool ready = protectedReadyToRunVariable;
pthread_mutex_unlock(&mutex);
                                 pthread_mutex_lock(&mutex);
                                 protectedReadyToRuNVariable = true;
                                 pthread_mutex_unlock(&mutex);
                                 pthread_cond_signal(&cond1);
if (ready) {
pthread_cond_wait(&cond1); // uh o!

À ce stade, il n'y a pas de thread qui va signaler la variable de condition, donc thread1 attendra indéfiniment, même si le protectedReadyToRunVariable dit qu'il est prêt à partir!

La seule façon de contourner ce problème est que les variables de condition libèrent atomiquement le mutex tout en commençant simultanément à attendre la variable de condition. C'est pourquoi la fonction cond_wait nécessite un mutex

// correct usage:
// thread 1:
while (notDone) {
    pthread_mutex_lock(&mutex);
    bool ready = protectedReadyToRunVariable
    if (ready) {
        pthread_mutex_unlock(&mutex);
        doWork();
    } else {
        pthread_cond_wait(&mutex, &cond1);
    }
}

// signalling thread
// thread 2:
prepareToRunThread1();
pthread_mutex_lock(&mutex);
   protectedReadyToRuNVariable = true;
   pthread_cond_signal(&mutex, &cond1);
pthread_mutex_unlock(&mutex);

3

Le mutex est censé être verrouillé lorsque vous appelez pthread_cond_wait; quand vous l'appelez, il déverrouille atomiquement le mutex et se bloque ensuite sur la condition. Une fois que la condition est signalée, elle la verrouille à nouveau et revient.

Cela permet la mise en œuvre d'une planification prévisible si on le souhaite, en ce que le thread qui ferait la signalisation peut attendre que le mutex soit libéré pour effectuer son traitement, puis signaler la condition.


Alors… y a-t-il une raison pour moi de ne pas simplement laisser le mutex toujours déverrouillé, puis de le verrouiller juste avant d'attendre, puis de le déverrouiller juste après la fin de l'attente?
ELLIOTTCABLE

Le mutex résout également certaines courses potentielles entre les threads d'attente et de signalisation. tant que le mutex est toujours verrouillé lors du changement de condition et de la signalisation, vous ne vous retrouverez jamais à manquer le signal et à dormir pour toujours
Hasturkun

Alors… je devrais d' abord attendre sur mutex sur le mutex de la variable de condition, avant d'attendre sur la variable de condition? Je ne suis pas sûr de comprendre du tout.
ELLIOTTCABLE

2
@elliottcable: Sans tenir le mutex, comment pourriez-vous savoir si vous devriez ou ne devriez pas attendre? Et si ce que vous attendez venait de se passer?
David Schwartz

1

J'ai fait un exercice en classe si vous voulez un vrai exemple de variable de condition:

#include "stdio.h"
#include "stdlib.h"
#include "pthread.h"
#include "unistd.h"

int compteur = 0;
pthread_cond_t varCond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex_compteur;

void attenteSeuil(arg)
{
    pthread_mutex_lock(&mutex_compteur);
        while(compteur < 10)
        {
            printf("Compteur : %d<10 so i am waiting...\n", compteur);
            pthread_cond_wait(&varCond, &mutex_compteur);
        }
        printf("I waited nicely and now the compteur = %d\n", compteur);
    pthread_mutex_unlock(&mutex_compteur);
    pthread_exit(NULL);
}

void incrementCompteur(arg)
{
    while(1)
    {
        pthread_mutex_lock(&mutex_compteur);

            if(compteur == 10)
            {
                printf("Compteur = 10\n");
                pthread_cond_signal(&varCond);
                pthread_mutex_unlock(&mutex_compteur);
                pthread_exit(NULL);
            }
            else
            {
                printf("Compteur ++\n");
                compteur++;
            }

        pthread_mutex_unlock(&mutex_compteur);
    }
}

int main(int argc, char const *argv[])
{
    int i;
    pthread_t threads[2];

    pthread_mutex_init(&mutex_compteur, NULL);

    pthread_create(&threads[0], NULL, incrementCompteur, NULL);
    pthread_create(&threads[1], NULL, attenteSeuil, NULL);

    pthread_exit(NULL);
}

1

Cela semble être une décision de conception spécifique plutôt qu'un besoin conceptuel.

Selon la documentation de pthreads, la raison pour laquelle le mutex n'a pas été séparé est qu'il y a une amélioration significative des performances en les combinant et ils s'attendent à ce qu'en raison de conditions de course courantes si vous n'utilisez pas de mutex, cela sera presque toujours fait de toute façon.

https://linux.die.net/man/3/pthread_cond_wait

Caractéristiques des mutex et des variables de condition

Il avait été suggéré que l'acquisition et la libération de mutex soient découplées de l'attente de condition. Cela a été rejeté car c'est la nature combinée de l'opération qui, en fait, facilite les implémentations en temps réel. Ces implémentations peuvent déplacer de manière atomique un thread de haute priorité entre la variable de condition et le mutex d'une manière transparente pour l'appelant. Cela peut empêcher des changements de contexte supplémentaires et fournir une acquisition plus déterministe d'un mutex lorsque le thread en attente est signalé. Ainsi, les questions d'équité et de priorité peuvent être traitées directement par la discipline de planification. En outre, l'opération d'attente de condition actuelle correspond à la pratique existante.


1

Il y a des tonnes d'exégèse à ce sujet, mais je veux le résumer avec un exemple suivant.

1 void thr_child() {
2    done = 1;
3    pthread_cond_signal(&c);
4 }

5 void thr_parent() {
6    if (done == 0)
7        pthread_cond_wait(&c);
8 }

Quel est le problème avec l'extrait de code? Réfléchissez un peu avant de continuer.


Le problème est vraiment subtil. Si le parent invoque thr_parent()puis vérifie la valeur de done, il verra que c'est le cas 0et essaiera ainsi de s'endormir. Mais juste avant d'appeler wait to go end, le parent est interrompu entre les lignes 6-7 et l'enfant s'exécute. L'enfant change la variable d'état doneen 1et signale, mais aucun thread n'attend et donc aucun thread n'est réveillé. Lorsque le parent s'exécute à nouveau, il dort pour toujours, ce qui est vraiment flagrant.

Et si elles sont effectuées alors que les serrures acquises individuellement?


C'est la vraie réponse.
Validus Oculus
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.