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_wait
appel (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.
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_data
juste 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)
la source
while (some_data != NULL)
être une boucle do-while pour qu'il attende la variable de condition au moins une fois?while(some_data != NULL)
êtrewhile(some_data == NULL)
?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.
la source
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:
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.
la source
pthread_cond_broadcast
et lespthread_cond_signal
opé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.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:signal()
avant lewait()
et nous manquerions ce réveil.check()
dépend normalement de la modification d'un autre thread, vous avez donc besoin d'une exclusion mutuelle de toute façon.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 loopedcheck()
.la source
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);
la source
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.
la source
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); }
la source
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
la source
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 dedone
, il verra que c'est le cas0
et 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'étatdone
en1
et 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?
la source