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

Je lis sur pthread.h ; les fonctions liées à la variable de condition (comme pthread_cond_wait(3) ) nécessitent un mutex comme argument. Pourquoi? Autant que je sache, je vais créer un mutex juste pour l’utiliser comme argument? Qu’est-ce que ce mutex est censé faire?

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 va déverrouiller “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, un ou plusieurs des threads de la liste d’attente seront réveillés et le mutex sera de nouveau verrouillé comme 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 auquel est atsortingbué un 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 est effectué dans cette boucle à condition que certains soient disponibles lorsque l’attente sera de retour. Lorsque le thread a été marqué pour cesser de faire le travail (généralement par un autre thread définissant la condition de sortie puis envoyant la variable condition pour réactiver ce thread), la boucle se terminera, le mutex sera déverrouillé et ce thread quittera.

Le code ci-dessus est un modèle à consommateur unique, car le mutex rest verrouillé pendant le travail. Pour une variante multi-consommateurs, vous pouvez par exemple utiliser:

 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 aux autres consommateurs de recevoir du travail pendant que celui-ci travaille.

La variable de condition vous évite d’avoir à interroger certaines conditions, ce qui permet à un autre thread de vous avertir en cas de problème. Un autre thread peut dire que le thread qui travaille 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 wake-ups intempestifs était généralement due au fait que plusieurs threads avaient été signalés dans leur appel pthread_cond_wait (broadcast), on retournait avec le mutex, faisait le travail, puis attendait à nouveau.

Ensuite, le deuxième thread signalé pourrait sortir quand il n’y avait pas de travail à faire. Vous deviez donc avoir une variable supplémentaire indiquant que le travail devrait être fait (ceci était insortingnsèquement protégé contre les mutex avec la paire condvar / mutex ici – d’autres threads étaient nécessaires pour verrouiller le mutex avant de le changer).

Il était techniquement possible qu’un thread revienne d’une condition d’attente sans être poussé par un autre processus (il s’agit d’un véritable réveil fallacieux), mais pendant toutes mes années de travail sur pthreads, à la fois développement / service du code et utilisateur d’entre eux, je n’en ai jamais reçu une seule fois. Peut-être que c’était juste parce que HP avait une implémentation décente 🙂

En tout état de cause, le même code qui traitait le cas erroné traitait également de véritables réverbérations fausses puisque l’indicateur de disponibilité de travail n’était pas défini pour ceux-ci.

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 à une condition 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 réveil fallacieux . Cela signifie que vous devez vérifier un prédicat, de sorte que vous pouvez être sûr que la condition a bien été signalée – et distinguer cela d’un réveil fallacieux. La vérification d’une telle condition en ce qui concerne l’attente doit être surveillée. Ainsi, une variable d’état doit pouvoir attendre / se réveiller de manière atomique tout en verrouillant / déverrouillant un mutex protégeant cette condition.

Prenons un exemple simple où vous êtes averti que certaines données sont produites. Peut-être qu’un autre thread a créé certaines données et défini un pointeur sur 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 conditions de course, et si l’autre thread faisait some_data = new_data juste après que vous ayez été réveillé, mais avant que vous ayez fait data = some_data

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

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

Ça ne marchera pas, il y a encore une chance de se retrouver dans une situation de course entre le réveil et le mutex. Placer le mutex avant le pthread_cond_wait ne vous aide pas, car vous allez maintenant tenir le mutex en attendant – c’est-à-dire que le producteur ne pourra jamais saisir le mutex. (Remarque: dans ce cas, vous pouvez créer une deuxième variable de condition pour signaler au producteur que vous avez fini avec 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 de manière atomique le mutex lorsque vous attendez ou que vous vous réveillez. Voilà ce que font les variables de condition de 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 aurait naturellement besoin de prendre les mêmes précautions, en gardant toujours «some_data» avec le même mutex, et en s’assurant qu’il ne remplace pas some_data si some_data est actuellement! = NULL)

Les variables de condition POSIX sont sans état. Il est donc de votre responsabilité de maintenir l’état. Comme les deux threads qui attendent et les threads qui indiquent aux autres threads d’attendre qu’ils accèdent à l’état, ils doivent être protégés 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 changent cette condition. Par exemple, un thread peut attendre que certaines données arrivent. 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 thread attend le travail. Le travail est protégé par un mutex. L’attente libère le mutex de sorte 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 queue de travail. Notez que la variable de condition elle-même n’a aucune idée s’il y a du travail ou non. En d’autres termes, une variable de condition doit être associée à une condition, cette condition doit être gérée par votre code et, comme elle est partagée entre les threads, elle doit être protégée par un mutex.

Toutes les fonctions de variable de condition ne nécessitent pas un 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 de manière permanente à un mutex spécifique; le mutex externe ne protège pas la variable de condition. Si une variable de condition a un état interne, par exemple une queue de threads en attente, elle doit être protégée par un verrou interne dans la variable de condition.

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

  • un thread a verrouillé le mutex, a évalué une expression sur des variables partagées et l’a trouvé faux, de sorte qu’il doit attendre.
  • le thread doit passer de la propriété du mutex à l’attente de la condition.

Pour cette raison, l’attente prend comme arguments à la fois le mutex et la condition: afin de pouvoir gérer le transfert atomique d’un thread qui possède le mutex en attente, de sorte que le thread ne soit pas victime de la condition de réveil perdue .

Une condition de course de réveil perdue se produira si un thread abandonne un mutex, puis attend un object de synchronisation sans état, mais d’une manière non atomique: il existe une fenêtre temporelle où le thread n’a plus le verrou et a pas encore commencé à attendre sur l’object. Pendant cette fenêtre, un autre thread peut entrer, rendre la condition attendue vraie, signaler la synchronisation sans état puis disparaître. L’object sans état ne se souvient pas qu’il a été signalé (il est sans état). Ainsi, le thread d’origine s’endort sur l’object de synchronisation sans état et ne se réveille pas, même si la condition requirejse est déjà vraie: perte de réveil.

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

Le mutex est censé être verrouillé lorsque vous appelez pthread_cond_wait ; quand vous l’appelez, il déverrouille de manière atomique le mutex et bloque 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 nécessaire, dans la mesure où le thread qui ferait la signalisation peut attendre que le mutex soit libéré pour effectuer son traitement, puis signaler la condition.

Les variables de condition sont associées à un mutex car c’est le seul moyen d’éviter la course à laquelle il est destiné.

 // 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 condition, donc thread1 attendra pour toujours, même si la propriété protectedReadyToRunVariable dit qu’il est prêt à partir!

Le seul moyen de contourner cela est que les variables de condition libèrent de manière atomique le mutex tout en commençant à 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); 

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

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

Il y a trois raisons pour emballer l’ wait() dans un mutex:

  1. sans un mutex, un autre thread pourrait signal() avant l’ wait() et nous manquerions ce réveil.
  2. Normalement, check() dépend de la modification d’un autre thread, vous devez donc l’exclure mutuellement.
  3. pour s’assurer que le thread de priorité la plus élevée procède en premier (la queue du mutex permet au planificateur de décider qui va suivre).

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

Des réveils fallacieux sont souvent mentionnés en ce qui concerne ce mécanisme (le thread en attente est réveillé sans que signal() soit appelé). Cependant, ces événements sont gérés par la check() boucle check() .

J’ai fait un exercice en classe si vous voulez un exemple réel 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); } 

Il semble s’agir d’une décision de conception spécifique plutôt que d’un besoin conceptuel.

Pour les pthreads, la raison pour laquelle le mutex n’a pas été séparé est qu’il ya une amélioration significative des performances en les combinant et qu’ils s’attendent à ce qu’en raison des conditions de course habituelles, si vous n’utilisez pas de mutex, cela se produise presque toujours.

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

Caractéristiques des mutex et des variables de condition

Il a été suggéré que l’acquisition et la libération du mutex soient découplées de l’attente de la 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 permettre 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. De plus, l’opération d’attente de condition en cours correspond à la pratique existante.