web-dev-qa-db-fra.com

Quand une variable de condition est-elle nécessaire, un mutex n'est-il pas suffisant?

Je suis sûr que mutex ne suffit pas, c'est la raison pour laquelle le concept de variables de condition existe; mais ça me bat et je ne peux pas me convaincre avec un scénario concret quand une variable de condition est essentielle.

Différences entre les variables conditionnelles, les mutex et les verrous la réponse acceptée à la question indique qu'une variable de condition est un

serrure avec mécanisme de "signalisation". Il est utilisé lorsque les threads doivent attendre qu'une ressource soit disponible. Un thread peut "attendre" un CV, puis le producteur de ressources peut "signaler" la variable, auquel cas les threads qui attendent le CV sont notifiés et peuvent continuer l'exécution

Là où je suis confus, c'est qu'un thread peut également attendre sur un mutex, et lorsqu'il est signalé, c'est simplement que la variable est maintenant disponible, pourquoi aurais-je besoin d'une variable de condition?

P.S .: De plus, un mutex est de toute façon requis pour protéger la variable de condition, ce qui rend ma vision plus inclinée vers le fait de ne pas voir le but de la variable de condition.

45
legends2k

Même si vous pouvez les utiliser de la manière que vous décrivez, les mutex n'ont pas été conçus pour être utilisés comme mécanisme de notification/synchronisation. Ils sont destinés à fournir un accès mutuellement exclusif à une ressource partagée. L'utilisation de mutex pour signaler une condition est maladroite et je suppose que cela ressemblerait à quelque chose comme ça (où Thread1 est signalé par Thread2):

Thread1:

while(1) {
    lock(mutex); // Blocks waiting for notification from Thread2
    ... // do work after notification is received
    unlock(mutex); // Tells Thread2 we are done
}

Thread2:

while(1) {
    ... // do the work that precedes notification
    unlock(mutex); // unblocks Thread1
    lock(mutex); // lock the mutex so Thread1 will block again
}

Il y a plusieurs problèmes avec ceci:

  1. Thread2 ne peut pas continuer à "effectuer le travail qui précède la notification" tant que Thread1 n'a pas terminé avec "travail après notification". Avec cette conception, Thread2 n'est même pas nécessaire, c'est-à-dire pourquoi ne pas déplacer "travail qui précède" et "travail après notification" dans le même thread car un seul peut s'exécuter à un moment donné!
  2. Si Thread2 n'est pas en mesure de préempter Thread1, Thread1 verrouillera immédiatement le mutex lorsqu'il répétera la boucle while (1) et Thread1 fera le "travail après notification" même s'il n'y a pas eu de notification. Cela signifie que vous devez en quelque sorte garantir que Thread2 verrouillera le mutex avant Thread1. Comment tu fais ça? Peut-être forcer un événement de planification en dormant ou par un autre moyen spécifique au système d'exploitation, mais même cela ne garantit pas de fonctionner en fonction du calendrier, de votre système d'exploitation et de l'algorithme de planification.

Ces deux problèmes ne sont pas mineurs, en fait, ce sont à la fois des défauts de conception majeurs et des bugs latents. L'origine de ces deux problèmes est l'exigence qu'un mutex soit verrouillé et déverrouillé dans le même thread. Alors, comment éviter les problèmes ci-dessus? Utilisez des variables de condition!

BTW, si vos besoins de synchronisation sont vraiment simples, vous pouvez utiliser un ancien sémaphore simple qui évite la complexité supplémentaire des variables de condition.

28
slowjelj

Mutex est pour l'accès exclusif à la ressource partagée, tandis que la variable conditionnelle est pour attendre qu'une condition soit vraie. Les gens peuvent penser qu'ils peuvent implémenter une variable conditionnelle sans le support du noyau. Une solution courante que l'on pourrait trouver est que le "flag + mutex" est comme:

lock(mutex)

while (!flag) {
    sleep(100);
}

unlock(mutex)

do_something_on_flag_set();

mais cela ne fonctionnera jamais, car vous ne relâchez jamais le mutex pendant l'attente, personne d'autre ne peut définir l'indicateur de manière thread-safe. C'est pourquoi nous avons besoin d'une variable conditionnelle, lorsque vous attendez une variable de condition, le mutex associé n'est pas maintenu par votre thread jusqu'à ce qu'il soit signalé.

7
Dagang

J'y pensais aussi et l'information la plus importante, qui me manquait partout était que mutex ne pouvait posséder (et changer) qu'à l'époque un seul thread. Donc, si vous avez un producteur et plus de consommateurs, le producteur devra attendre le mutex pour produire. Avec cond. variable qu'il peut produire à tout moment.

5
MBI

Le var conditionnel et la paire mutex peuvent être remplacés par un sémaphore binaire et une paire mutex. La séquence d'opérations d'un thread consommateur lors de l'utilisation du mutex var + conditionnel est la suivante:

  1. Verrouillez le mutex

  2. Attendez le var conditionnel

  3. Processus

  4. Déverrouillez le mutex

La séquence d'opérations du thread producteur est

  1. Verrouillez le mutex

  2. Signaler la var conditionnelle

  3. Déverrouillez le mutex

La séquence de thread de consommateur correspondante lors de l'utilisation de la paire sema + mutex est

  1. Attendez sur le sema binaire

  2. Verrouillez le mutex

  3. Vérifiez la condition attendue

  4. Si la condition est vraie, procédez.

  5. Déverrouillez le mutex

  6. Si la vérification des conditions à l'étape 3 était fausse, revenez à l'étape 1.

La séquence du thread producteur est la suivante:

  1. Verrouillez le mutex

  2. Publier le sema binaire

  3. Déverrouillez le mutex

Comme vous pouvez le voir, le traitement inconditionnel à l'étape 3 lors de l'utilisation du var conditionnel est remplacé par le traitement conditionnel à l'étape 3 et l'étape 4 lors de l'utilisation du sema binaire.

La raison en est que lorsque vous utilisez sema + mutex, dans une condition de concurrence critique, un autre thread consommateur peut se faufiler entre les étapes 1 et 2 et traiter/annuler la condition. Cela ne se produira pas lors de l'utilisation de var conditionnelle. Lorsque vous utilisez la variable conditionnelle, la condition est garantie comme vraie après l'étape 2.

Le sémaphore binaire peut être remplacé par le sémaphore de comptage régulier. Cela peut entraîner plusieurs fois la boucle de l'étape 6 à l'étape 1.

3
Frank M

Vous avez besoin de variables de condition, à utiliser avec un mutex (chaque condition.var appartient à un mutex) pour signaler des états changeants (conditions) d'un thread à un autre. L'idée est qu'un thread peut attendre qu'une certaine condition devienne vraie. Ces conditions sont spécifiques au programme (c'est-à-dire "la file d'attente est vide", "la matrice est grande", "certaines ressources sont presque épuisées", "une étape de calcul est terminée", etc.). Un mutex peut avoir plusieurs variables de condition liées. Et vous avez besoin de variables de condition car de telles conditions peuvent ne pas toujours être exprimées aussi simplement que "un mutex est verrouillé" (vous devez donc diffuser les changements de conditions sur d'autres threads).

Lisez quelques bons tutoriels sur les threads posix, par exemple ce tutoriel ou que ou que un. Mieux encore, lisez un bon livre pthread. Voir cette question .

Lisez également Programmation Unix avancée et Programmation Linux avancée

P.S. Le parallélisme et les fils sont des concepts difficiles à saisir. Prenez le temps de lire et d'expérimenter et de relire.

2

Je pense que c'est la mise en œuvre définie.
Le mutex est suffisant ou non dépend si vous considérez le mutex comme un mécanisme pour les sections critiques ou quelque chose de plus.

Comme mentionné dans http://en.cppreference.com/w/cpp/thread/mutex/unlock ,

Le mutex doit être verrouillé par le thread d'exécution en cours, sinon le comportement n'est pas défini.

ce qui signifie qu'un thread ne pouvait déverrouiller qu'un mutex qui était verrouillé/détenu par lui-même en C++.
Mais dans d'autres langages de programmation, vous pourriez être en mesure de partager un mutex entre les processus.

Ainsi, la distinction des deux concepts peut n'être que des considérations de performances, une identification de propriété complexe ou un partage inter-processus ne sont pas dignes pour des applications simples.


Par exemple, vous pouvez corriger le cas de @ slowjelj avec un mutex supplémentaire (il peut s'agir d'un correctif incorrect):

Thread1:

lock(mutex0);
while(1) {
    lock(mutex0); // Blocks waiting for notification from Thread2
    ... // do work after notification is received
    unlock(mutex1); // Tells Thread2 we are done
}

Thread2:

while(1) {
    lock(mutex1); // lock the mutex so Thread1 will block again
    ... // do the work that precedes notification
    unlock(mutex0); // unblocks Thread1
}

Mais votre programme se plaindra que vous avez déclenché une assertion laissée par le compilateur (par exemple "déverrouiller un mutex sans propriétaire" dans Visual Studio 2015).

0
Mr. Ree