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

POSIX permet aux mutex d’être récursifs. Cela signifie que le même thread peut verrouiller deux fois le même mutex et ne pas se bloquer. 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 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 généralement des mutex, souvent appelés Locks. Certains systèmes / langages (par exemple, Cocoa Objective-C) offrent des mutex à la fois 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 object). Selon les autres fonctionnalités de threads proposées, le fait de ne pas avoir de mutex récursifs ne pose aucun problème, car elles peuvent facilement être écrites vous-même (j’ai déjà implémenté des mutex récursifs 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 une impasse de thread si elle 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 lancer une exception si c’est le cas) ne le font généralement pas. Ils laisseront le fil à l’impasse à la place.

Est-ce seulement pour les cas où je le verrouille deux fois par inadvertance et ne le déverrouille qu’une seule fois et en cas de mutex récursif, il serait plus difficile de trouver le problème? Mais est-ce que je ne pourrais pas faire la même chose avec un compteur verrouillé lors du délocking et dans une situation où je suis sûr d’avoir libéré le dernier verrou et le compteur non nul, je peux lancer une exception ou enregistrer le problème? Ou existe-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 une performance, car un mutex non récursif peut être légèrement plus rapide qu’un mutex récursif? Cependant, j’ai testé ceci et la différence n’est vraiment pas si grande.

    La différence entre un mutex récursif et non récursif est liée à la propriété. Dans le cas d’un mutex récursif, le kernel doit garder une trace du thread qui a effectivement obtenu le mutex la première fois afin qu’il puisse détecter la différence entre une récursivité et un thread différent qui devrait bloquer à la place. Comme une autre réponse l’a fait remarquer, il s’agit de la surcharge supplémentaire en termes de mémoire pour stocker ce contexte et des cycles nécessaires à sa maintenance.

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

    Étant donné que le mutex récursif a un sens de propriété, le thread qui saisit le mutex doit être le même thread qui libère le mutex. Dans le cas de mutex non récursifs, il n’y a pas de sentiment d’appartenance et tout 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 plutôt une action sémaphore, où vous n’utilisez pas nécessairement le mutex comme périphérique d’exclusion mais l’utilisez comme périphérique de synchronisation ou de signalisation entre deux threads ou plus.

    Une autre propriété associée à un sentiment d’appartenance à un mutex est la capacité à prendre en charge l’inheritance prioritaire. Comme le kernel peut suivre le thread qui possède le mutex et l’identité de tous les bloqueurs, il devient possible d’élever la priorité du thread qui possède actuellement le mutex dans la priorité du thread de priorité la plus élevée. cela bloque actuellement le mutex. Cet inheritance évite le problème d’inversion de priorité pouvant survenir dans de tels cas. (Notez que tous les systèmes ne prennent pas en charge l’inheritance prioritaire sur de tels mutex, mais c’est une autre caractéristique qui devient possible via la notion de propriété).

    Si vous faites référence au kernel RTOS classique de VxWorks, ils définissent trois mécanismes:

    • mutex – supporte la récursivité, et éventuellement l’inheritance prioritaire
    • sémaphore binary – pas de récursivité, pas d’inheritance, exclusion simple, preneur et donneur ne doit pas nécessairement être le même thread, version de diffusion disponible
    • le sémaphore de comptage – pas de récursivité ou d’inheritance, agit comme un compteur de ressources cohérent à partir de n’importe quel nombre initial souhaité, les threads ne bloquent que lorsque le compte net par rapport à la ressource est 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.

    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é quand vous l’avez écrit. Mais 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, ces impasses. Si vous les avez, ça marche, mais ça peut casser. Un :: foo () peut avoir laissé l’object dans un état incohérent avant d’appeler bar (), en supposant que baz () ne pourrait pas être exécuté car il acquiert également le mutex. Mais ça ne devrait probablement pas courir! 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 utiliser les 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 serrures réentrantes sont dangereuses 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 à déboguer un problème comme celui-ci auparavant. Java a des verrous non réentrants ces jours-ci dans java.util.concurrent.locks, au fait.

    Comme écrit par 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 la trace de votre système de locking et de votre scope. C’est mortel. Le mal, c’est le” mangeur de fil “. Si tu appelles quelque chose avec une serrure simplement parce que tu ne sais pas que c’est tenu, ou parce que tu ne sais pas si l’appelé a besoin du mutex, alors tu le tiens trop longtemps. viser un fusil de chasse sur votre application et en tirant la gâchette. Vous avez probablement commencé à utiliser des threads pour obtenir la concurrence, mais vous venez de prévenir la concurrence. ”

    Le bon modèle mental pour utiliser les mutex: Le mutex protège un invariant.

    Pourquoi êtes-vous sûr que c’est vraiment un bon modèle mental pour utiliser des 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 avez besoin de protéger les invariants, vous pouvez toujours utiliser un sémaphore binary qui n’est jamais récursif.

    L’une des principales raisons pour lesquelles les mutexes récursifs sont utiles est dans le cas où les méthodes d’access à la méthode sont multiples par le même thread. Par exemple, si mutex lock protège une banque A / c pour se retirer, alors si une redevance est également associée à ce retrait, alors le même mutex doit être utilisé.

    Le seul cas d’utilisation approprié pour le mutex de récurrence est lorsqu’un object contient plusieurs méthodes. Lorsque l’une des méthodes modifie le contenu de l’object et doit par conséquent verrouiller l’object 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 finalise avec recheckBounds ()), mais chacune de ces fonctions doit verrouiller le mutex, alors le mutex récursif est gagnant-gagnant.

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