Verrou récursif (Mutex) vs Verrou non récursif (Mutex)

183

POSIX permet aux mutex d'être récursifs. Cela signifie que le même thread peut verrouiller le même mutex deux fois et ne sera pas bloqué. Bien sûr, il doit également le déverrouiller deux fois, sinon aucun autre thread ne peut obtenir le mutex. Tous les systèmes prenant en charge les pthreads ne prennent pas également en charge les mutex récursifs, mais s'ils veulent être conformes à POSIX, ils doivent le faire .

D'autres API (API de plus haut niveau) proposent également généralement des mutex, souvent appelés Locks. Certains systèmes / langages (par exemple Cocoa Objective-C) offrent à la fois des mutex récursifs et non récursifs. Certaines langues n'offrent également que l'une ou l'autre. Par exemple, en Java, les mutex sont toujours récursifs (le même thread peut se "synchroniser" deux fois sur le même objet). En fonction des autres fonctionnalités de thread qu'ils offrent, ne pas avoir de mutex récursifs pourrait ne pas poser de problème, car ils peuvent facilement être écrits vous-même (j'ai déjà implémenté des mutex récursifs moi-même sur la base d'opérations mutex / condition plus simples).

Ce que je ne comprends pas vraiment: à quoi servent les mutex non récursifs? Pourquoi voudrais-je avoir un blocage de thread s'il verrouille deux fois le même mutex? Même les langages de haut niveau qui pourraient éviter cela (par exemple, tester si cela va bloquer et lever une exception si c'est le cas) ne le font généralement pas. Ils laisseront le thread bloqué à la place.

Est-ce uniquement pour les cas où je le verrouille accidentellement deux fois et ne le déverrouille qu'une seule fois et dans le cas d'un mutex récursif, il serait plus difficile de trouver le problème, donc à la place, je le verrouille immédiatement pour voir où le verrou incorrect apparaît? Mais ne pourrais-je pas faire la même chose avec le retour d'un compteur de verrouillage lors du déverrouillage et dans une situation où je suis sûr que j'ai relâché le dernier verrou et que le compteur n'est pas nul, je peux lever une exception ou enregistrer le problème? Ou y a-t-il un autre cas d'utilisation plus utile de mutex non récursifs que je ne vois pas? Ou est-ce peut-être juste des performances, car un mutex non récursif peut être légèrement plus rapide qu'un mutex récursif? Cependant, j'ai testé cela et la différence n'est vraiment pas si grande.

Mecki
la source

Réponses:

154

La différence entre un mutex récursif et non récursif a à voir avec la propriété. Dans le cas d'un mutex récursif, le noyau doit garder une trace du thread qui a réellement obtenu le mutex la première fois afin de pouvoir détecter la différence entre la récursivité et un thread différent qui devrait bloquer à la place. Comme l'a souligné une autre réponse, il y a une question de surcoût supplémentaire à la fois en termes de mémoire pour stocker ce contexte et aussi des cycles nécessaires pour le maintenir.

Cependant , il y a d'autres considérations en jeu ici aussi.

Étant donné que le mutex récursif a un sentiment de propriété, le thread qui saisit le mutex doit être le même thread qui libère le mutex. Dans le cas des mutex non récursifs, il n'y a pas de sens de propriété et n'importe quel thread peut généralement libérer le mutex, quel que soit le thread qui a pris le mutex à l'origine. Dans de nombreux cas, ce type de "mutex" est en réalité plus une action de sémaphore, où vous n'utilisez pas nécessairement le mutex comme un dispositif d'exclusion mais l'utilisez comme dispositif de synchronisation ou de signalisation entre deux ou plusieurs threads.

Une autre propriété qui vient avec un sentiment de propriété dans un mutex est la capacité de prendre en charge l'héritage prioritaire. Étant donné que le noyau peut suivre le thread propriétaire du mutex ainsi que l'identité de tous les bloqueurs, dans un système threadé prioritaire, il devient possible d'élever la priorité du thread qui possède actuellement le mutex à la priorité du thread de priorité la plus élevée qui bloque actuellement sur le mutex. Cet héritage évite le problème d'inversion de priorité qui peut se produire dans de tels cas. (Notez que tous les systèmes ne prennent pas en charge l'héritage de priorité sur de tels mutex, mais c'est une autre fonctionnalité qui devient possible via la notion de propriété).

Si vous vous référez au noyau VxWorks RTOS classique, ils définissent trois mécanismes:

  • mutex - prend en charge la récursivité et éventuellement l'héritage de priorité. Ce mécanisme est couramment utilisé pour protéger de manière cohérente des sections critiques de données.
  • sémaphore binaire - pas de récursivité, pas d'héritage, exclusion simple, preneur et donneur ne doivent pas nécessairement être le même fil, version de diffusion disponible Ce mécanisme peut être utilisé pour protéger des sections critiques, mais il est également particulièrement utile pour une signalisation cohérente ou une synchronisation entre threads.
  • comptage de sémaphore - pas de récursivité ni d'héritage, agit comme un compteur de ressources cohérent à partir de tout compte initial souhaité, les threads ne bloquent que lorsque le compte net par rapport à la ressource est égal à zéro.

Encore une fois, cela varie quelque peu selon la plate-forme - en particulier ce qu'ils appellent ces choses, mais cela devrait être représentatif des concepts et des divers mécanismes en jeu.

Grand Jeff
la source
9
votre explication sur le mutex non récursif ressemblait plus à un sémaphore. Un mutex (qu'il soit récursif ou non récursif) a une notion de propriété.
Jay D
@JayD C'est très déroutant quand les gens se disputent à propos de choses comme celles-ci ... alors qui est l'entité qui définit ces choses?
Pacerier
13
@Pacerier La norme pertinente. Cette réponse est par exemple fausse pour posix (pthreads), où le déverrouillage d'un mutex normal dans un thread autre que le thread qui l'a verrouillé est un comportement indéfini, alors que faire la même chose avec une vérification d'erreur ou un mutex récursif entraîne un code d'erreur prévisible. D'autres systèmes et normes peuvent se comporter de manière très différente.
nos
C'est peut-être naïf, mais j'avais l'impression que l'idée centrale d'un mutex est que le fil de verrouillage déverrouille le mutex et que d'autres threads peuvent faire de même. De computing.llnl.gov/tutorials/pthreads :
user657862
2
@curiousguy - une version de diffusion libère tous les threads bloqués sur le sémaphore sans le donner explicitement (reste vide) alors qu'une donnée binaire normale ne libérerait que le thread en tête de la file d'attente (en supposant qu'il y en ait un bloqué).
Tall Jeff
123

La réponse n'est pas l' efficacité. Les mutex non réentrants conduisent à un meilleur code.

Exemple: A :: foo () acquiert le verrou. Il appelle ensuite B :: bar (). Cela a bien fonctionné lorsque vous l'avez écrit. Mais quelque temps plus tard, quelqu'un change B :: bar () pour appeler A :: baz (), qui acquiert également le verrou.

Eh bien, si vous n'avez pas de mutex récursifs, cette impasse. Si vous en avez, il fonctionne, mais il peut se casser. A :: foo () peut avoir laissé l'objet dans un état incohérent avant d'appeler bar (), en supposant que baz () ne peut pas être exécuté car il acquiert également le mutex. Mais il ne devrait probablement pas fonctionner! La personne qui a écrit A :: foo () a supposé que personne ne pouvait appeler A :: baz () en même temps - c'est la raison pour laquelle ces deux méthodes ont acquis le verrou.

Le bon modèle mental pour l'utilisation des mutex: Le mutex protège un invariant. Lorsque le mutex est maintenu, l'invariant peut changer, mais avant de libérer le mutex, l'invariant est rétabli. Les verrous réentrants sont dangereux car la deuxième fois que vous acquérez le verrou, vous ne pouvez plus être sûr que l'invariant est vrai.

Si vous êtes satisfait des verrous réentrants, c'est uniquement parce que vous n'avez pas eu à déboguer un problème comme celui-ci auparavant. Java a des verrous non réentrants ces jours-ci dans java.util.concurrent.locks, d'ailleurs.

Jonathan
la source
4
Il m'a fallu un certain temps pour comprendre ce que vous disiez au sujet de l'invariant n'étant pas valide lorsque vous saisissez la serrure une deuxième fois. Bon point! Et s'il s'agissait d'un verrou en lecture-écriture (comme ReadWriteLock de Java) et que vous aviez acquis le verrou de lecture, puis réacquis le verrou de lecture une deuxième fois dans le même thread. Vous n'invalideriez pas un invariant après avoir acquis un verrou de lecture, n'est-ce pas? Ainsi, lorsque vous acquérez le deuxième verrou de lecture, l'invariant est toujours vrai.
dgrant le
1
@Jonathan Est-ce que Java a des verrous non réentrants ces jours-ci dans java.util.concurrent.locks ??
user454322
1
+1 Je suppose que l'utilisation la plus courante du verrouillage réentrant est à l'intérieur d'une seule classe, où certaines méthodes peuvent être appelées à partir de morceaux de code protégés et non protégés. Cela peut en fait toujours être pris en compte. @ user454322 Bien sûr, Semaphore.
maaartinus
1
Pardonnez mon malentendu, mais je ne vois pas en quoi cela est pertinent pour le mutex. Supposons qu'il n'y ait pas de multithreading et de verrouillage impliqués, A::foo()peut avoir laissé l'objet dans un état incohérent avant l'appel A::bar(). Qu'est-ce que le mutex, récursif ou non, a quelque chose à voir avec ce cas?
Siyuan Ren
1
@SiyuanRen: Le problème est de pouvoir raisonner localement sur le code. Les gens (au moins moi) sont formés pour reconnaître les régions verrouillées comme un maintien invariant, c'est-à-dire qu'au moment où vous acquérez le verrou, aucun autre thread ne modifie l'état, donc les invariants de la région critique sont maintenus. Ce n'est pas une règle stricte, et vous pouvez coder sans tenir compte des invariants, mais cela rendrait simplement votre code plus difficile à raisonner et à maintenir. La même chose se produit en mode mono-thread sans mutex, mais là nous ne sommes pas formés pour raisonner localement autour de la région protégée.
David Rodríguez - dribeas
92

Comme l'écrit Dave Butenhof lui - même :

"Le plus gros de tous les gros problèmes avec les mutex récursifs est qu'ils vous encouragent à perdre complètement votre schéma de verrouillage et votre portée. C'est mortel. Mal. C'est le" mangeur de fil ". Vous maintenez les verrous pour le temps le plus court possible. Période. Toujours. Si vous appelez quelque chose avec un cadenas maintenu simplement parce que vous ne savez pas qu'il est tenu, ou parce que vous ne savez pas si l'appelé a besoin du mutex, alors vous le tenez trop longtemps. Vous êtes en visant votre application avec un fusil à pompe et en appuyant sur la gâchette. Vous avez probablement commencé à utiliser des threads pour obtenir la concurrence, mais vous venez de PRÉVENIR la concurrence. "

Chris Cleeland
la source
9
Notez également la dernière partie de la réponse de Butenhof: ...you're not DONE until they're [recursive mutex] all gone.. Or sit back and let someone else do the design.
user454322
2
Il dit également que l'utilisation d'un seul mutex récursif global (son opinion est que vous n'en avez besoin que d'un seul) est acceptable comme béquille pour reporter consciemment le dur travail de compréhension des invariances d'une bibliothèque externe lorsque vous commencez à l'utiliser dans du code multithread. Mais vous ne devriez pas utiliser de béquilles pour toujours, mais investir éventuellement du temps pour comprendre et corriger les invariants de concurrence du code. Nous pourrions donc paraphraser que l'utilisation du mutex récursif est une dette technique.
FooF le
13

Le bon modèle mental pour l'utilisation des mutex: Le mutex protège un invariant.

Pourquoi êtes-vous sûr que c'est vraiment le bon modèle mental pour l'utilisation de mutex? Je pense que le bon modèle protège les données mais pas les invariants.

Le problème de la protection des invariants se présente même dans les applications à un seul thread et n'a rien de commun avec le multi-threading et les mutex.

De plus, si vous devez protéger les invariants, vous pouvez toujours utiliser un sémaphore binaire qui n'est jamais récursif.


la source
Vrai. Il existe de meilleurs mécanismes pour protéger un invariant.
ActiveTrayPrntrTagDataStrDrvr
8
Cela devrait être un commentaire à la réponse qui a offert cette déclaration. Les mutex ne protègent pas seulement les données, ils protègent également les invariants. Essayez d'écrire un conteneur simple (le plus simple étant une pile) en termes d'atomes (où les données se protègent) au lieu de mutex et vous comprendrez la déclaration.
David Rodríguez - dribeas
Les mutex ne protègent pas les données, ils protègent un invariant. Cet invariant peut cependant être utilisé pour protéger les données.
Jon Hanna
4

Une des principales raisons pour lesquelles les mutex récursifs sont utiles est en cas d'accès aux méthodes plusieurs fois par le même thread. Par exemple, disons que si le verrouillage mutex protège une banque A / C pour se retirer, alors s'il y a des frais également associés à ce retrait, alors le même mutex doit être utilisé.

avis
la source
4

Le seul bon cas d'utilisation du mutex de récursivité est lorsqu'un objet contient plusieurs méthodes. Lorsque l'une des méthodes modifie le contenu de l'objet et doit donc verrouiller l'objet avant que l'état ne soit à nouveau cohérent.

Si les méthodes utilisent d'autres méthodes (par exemple: addNewArray () appelle addNewPoint () et se termine avec recheckBounds ()), mais que l'une de ces fonctions par elle-même doit verrouiller le mutex, alors le mutex récursif est gagnant-gagnant.

Pour tout autre cas (résoudre juste un mauvais codage, l'utiliser même dans différents objets) est clairement faux!

DarkZeros
la source
1

À quoi servent les mutex non récursifs?

Ils sont absolument bons lorsque vous devez vous assurer que le mutex est déverrouillé avant de faire quelque chose. C'est parce que pthread_mutex_unlockpeut garantir que le mutex est déverrouillé uniquement s'il est non récursif.

pthread_mutex_t      g_mutex;

void foo()
{
    pthread_mutex_lock(&g_mutex);
    // Do something.
    pthread_mutex_unlock(&g_mutex);

    bar();
}

Si g_mutexn'est pas récursif, le code ci-dessus est garanti pour appeler bar()avec le mutex déverrouillé .

Éliminant ainsi la possibilité d'une impasse en cas bar() il se trouverait une fonction externe inconnue qui pourrait bien faire quelque chose qui pourrait amener un autre thread à essayer d'acquérir le même mutex. De tels scénarios ne sont pas rares dans les applications construites sur des pools de threads et dans les applications distribuées, où un appel interprocessus peut engendrer un nouveau thread sans que le programmeur client ne s'en rende compte. Dans tous ces scénarios, il est préférable d'appeler lesdites fonctions externes uniquement après le déverrouillage.

Si g_mutexc'était récursif, il n'y aurait tout simplement aucun moyen de s'assurer qu'il est déverrouillé avant de passer un appel.

Igor G
la source