web-dev-qa-db-fra.com

Pourquoi les fonctions de variable de condition 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 à utiliser comme argument? Qu'est-ce que ce mutex est censé faire?

173
ELLIOTTCABLE

C'est simplement la façon dont les variables de condition sont (ou ont été à 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 de faire une attente.

L'attente déverrouillera "de manière atomique" le mutex, permettant à d'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, il sera réveillé et le mutex sera à 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 qui reçoit du travail via un signal vers 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 qu’il en reste lors du retour de l’attente. Lorsque le thread a été marqué pour qu'il cesse de travailler (généralement par un autre thread qui définit la condition de sortie puis envoie la variable condition pour réveiller ce thread), la boucle se ferme, le mutex est déverrouillé et ce thread se ferme.

Le code ci-dessus est un modèle à consommateur unique, car le mutex reste verrouillé pendant le travail. 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.

qui permet aux autres consommateurs de recevoir du travail pendant que celui-ci travaille.

La variable condition vous libère de la tâche d'interrogation d'une condition, ce qui permet à un autre thread de vous avertir lorsque quelque chose doit se produire. Un autre thread peut indiquer que ce thread fonctionne 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 "wakeups parasites" était généralement toujours due au fait que plusieurs threads avaient été signalés dans leur pthread_cond_wait appel, on reviendrait avec le mutex, ferait le travail, puis re-attendre.

Ensuite, le deuxième thread signalé pourrait sortir quand il n'y avait pas de travail à faire. Donc, vous deviez avoir une variable supplémentaire indiquant que le travail devait être effectué (ceci était intrinsèquement protégé par mutex avec la paire condvar/mutex ici - d'autres threads devaient verrouiller le mutex avant de le changer).

Il était techniquement possible de revenir d’un processus attendre sans être renvoyé par un autre processus (il s’agit d’un véritable réveil fallacieux), mais de nombreuses années de travail sur les pthreads, à la fois en développement/service du code et en tant qu’utilisateur, je n’en ai jamais reçu un. C'était peut-être juste parce que HP avait une implémentation décente :-)

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

183
paxdiablo

Une variable de condition est assez limitée si vous ne pouvez que signaler une condition; vous devez généralement gérer certaines données liées à la condition signalée. La signalisation/le réveil doit être fait de manière atomique pour y parvenir sans créer de conditions de concurrence, ou être trop complexe

pthreads peut également vous donner, pour des raisons plutôt techniques, un réveil parasite . Cela signifie que vous devez vérifier un prédicat afin de vous assurer que la condition a bien été signalée - et de le distinguer d'un réveil parasite. La vérification d'une telle condition en ce qui concerne l'attente doit être protégée - ainsi, une variable de condition a besoin d'un moyen d'attendre/de se réveiller de manière atomique lors du verrouillage/déverrouillage d'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éé les données que vous souhaitez et a défini un pointeur sur ces données.

Imaginez un thread producteur qui donne 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, et si l'autre thread fait some_data = new_data juste après que tu sois réveillé, mais avant toi 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);
}

Cela ne fonctionnera pas, il y a encore une chance d'une condition de concurrence entre le réveil et la saisie du 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 l'attraper. (notez que dans ce cas, vous pouvez créer une seconde variable de condition pour signaler au producteur que vous avez terminé avec some_data - bien que cela devienne complexe, surtout si vous voulez beaucoup de producteurs/consommateurs.)

Ainsi, vous avez besoin d’un moyen de libérer/saisir atomiquement le mutex lorsque vous attendez/sortez 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 devra naturellement prendre les mêmes précautions, en protégeant toujours "une_data" avec le même mutex et en s'assurant qu'il ne remplace pas une_data si une_data est actuellement! = NULL)

56
nos

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 aux threads qui attendent et aux threads qui ordonnent aux autres threads d'arrêter d'attendre, il doit être protégé par un mutex. Si vous pensez que vous pouvez 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 fil peut attendre que des 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 fil attend son travail. L'œuvre est protégée par un mutex. L'attente libère le mutex afin qu'un autre thread puisse lui donner du travail. 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 condition elle-même n'a aucune idée s'il y a du travail ou non. C'est-à-dire qu'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.

29
David Schwartz

Toutes les fonctions de variable de condition ne nécessitent pas de mutex: seules les opérations en attente le nécessitent. Les opérations de signal et de diffusion ne nécessitent pas de mutex. Une variable de condition n'est pas 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, telle qu'une file d'attente de threads en attente, elle doit être protégée par un verrou interne à l'intérieur de la variable de condition.

Les opérations d'attente réunissent 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 atomiquement passer de la possession du mutex à l'attente de la condition.

Pour cette raison, l'opération wait prend comme arguments le mutex et la condition: afin de pouvoir gérer le transfert atomique d'un thread de l'attribut propriétaire du mutex à l'attente, de sorte que le thread ne soit pas victime de la condition de course au réveil perdue .

Une situation de course de réveil perdue surviendra si un thread cède un mutex, puis attend un objet 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'objet. Au cours de 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 s'endort 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à remplie: 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 intercepter de manière fiable le réveil avant qu'il n'abandonne le mutex. Cela serait impossible si la fonction d'attente de variable de condition ne prenait pas le mutex comme argument.

13
Kaz

Je ne trouve pas que les autres réponses soient 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 envelopper la wait() dans un mutex:

  1. sans mutex, un autre thread pourrait signal() avant la wait() et nous manquerions ce réveil.
  2. normalement check() dépend 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 le plus prioritaire continue 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é à l’article cette conversation .

Les réveils parasites sont souvent mentionnés à propos de ce mécanisme (c'est-à-dire que le fil d'attente est réveillé sans que signal() ne soit appelée). Cependant, de tels événements sont gérés par la boucle check().

6
Sam Brightman

Les variables de condition sont associées à un mutex car c'est le seul moyen d'éviter la course pour laquelle il est conçu.

// 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, aucun thread ne signalera la variable de condition. Par conséquent, thread1 attendra indéfiniment, bien que protectedReadyToRunVariable indique qu'elle est prête à fonctionner!

La seule solution consiste pour les variables de condition à atomiquement libérer 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);
4
Cort Ammon

Le mutex est censé être verrouillé lorsque vous appelez pthread_cond_wait; lorsque vous l'appelez de manière atomique, le mutex est déverrouillé puis bloqué sur la condition. Une fois que la condition est signalée, elle se verrouille à nouveau atomiquement et revient.

Cela permet, si vous le souhaitez, de mettre en œuvre une planification prévisible, en ce sens que le thread qui ferait la signalisation peut attendre que le mutex soit libéré pour effectuer son traitement, puis signaler la condition.

3
Amber

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

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

Selon les auteurs, la raison pour laquelle le mutex n'a pas été séparé est qu'il y a une amélioration significative de leurs performances en les combinant et ils s'attendent à ce qu'en raison des conditions de concurrence 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 du mutex soient découplées de l'attente conditionnelle. 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 hautement prioritaire entre la variable de condition et le mutex de 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 l'établissement des horaires. En outre, l'opération d'attente de condition en cours correspond à la pratique existante.

1
Catskul

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

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? Juste réfléchir un peu avant d'aller de l'avant.


La question est véritablement subtile. Si le parent appelle thr_parent() et valide la valeur de done, il verra qu'il s'agit de 0 Et essaiera donc de s'endormir. Mais juste avant qu'il appelle, attend de s'endormir, le parent est interrompu entre les lignes 6-7 et l'enfant s'exécute. L'enfant change la variable d'état done en 1 Et le signale, mais aucun thread n'est en attente et aucun thread n'est donc réveillé. Quand le parent reparle, il dort pour toujours, ce qui est vraiment flagrant.

Que se passe-t-il si elles sont effectuées en même temps que les verrous acquis?

0
snr