web-dev-qa-db-fra.com

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 le même mutex deux fois 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 les pthreads ne prennent pas également en charge les mutex récursifs, mais s'ils veulent être conformes à POSIX, ils doivent) .

D'autres API (API de plus haut niveau) proposent également des mutex, souvent appelés verrous. 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. in Java sont toujours récursifs (le même thread peut se "synchroniser" deux fois sur le même objet). En fonction de la fonctionnalité des autres threads proposés, ne pas avoir de mutex récursif ne pose aucun problème, car peut facilement être écrit vous-même (j’ai déjà implémenté moi-même 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 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 aboutit à une impasse et renvoyer une exception le cas échéant) ne le font généralement pas. Ils laisseront le blocage du fil à la place.

N’est-ce que dans les cas où je le verrouille deux fois par inadvertance et ne le déverrouille qu’une seule fois; dans le cas d’un mutex récursif, il serait plus difficile de trouver le problème. Par conséquent, j’ai un blocage immédiat pour voir où apparaît le verrou incorrect. Mais est-ce que je ne pourrais pas faire la même chose avec un compteur de verrou retourné lors du déverrouillage et dans une situation où je suis sûr d'avoir relâché le dernier verrou et que le compteur n'est pas à zéro, puis-je déclencher une exception ou consigner le problème? Ou y a-t-il un autre cas d'utilisation plus utile de mutex non récursif que je ne vois pas? Ou bien s'agit-il simplement de performances, puisqu'un mutex non récursif peut être légèrement plus rapide qu'un muté récursif? Cependant, j'ai testé cela et la différence n'est vraiment pas si grande.

172
Mecki

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 l'a réellement obtenu la première fois afin qu'il puisse détecter la différence entre la récursivité et un autre thread qui devrait se bloquer. Comme une autre réponse l’a souligné, il existe une question de surcoût supplémentaire en termes de mémoire pour stocker ce contexte et de cycles nécessaires à sa maintenance.

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

Parce que le mutex récursif a un sens de propriété, le thread qui récupère le mutex doit être le même que celui 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 initialement pris le mutex. Dans de nombreux cas, ce type de "mutex" est en réalité une action de sémaphore, dans laquelle vous n'utilisez pas nécessairement le mutex en tant que périphérique d'exclusion, mais plutôt en tant que périphérique de synchronisation ou de signalisation entre deux threads ou plus.

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

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

  • mutex - prend en charge la récursivité et, éventuellement, l'héritage prioritaire. Ce mécanisme est couramment utilisé pour protéger les sections critiques de données de manière cohérente.
  • sémaphore binaire - pas de récursion, pas d'héritage, simple exclusion, preneur et donneur ne doivent pas obligatoirement être du même fil, diffusion complète disponible. Ce mécanisme peut être utilisé pour protéger des sections critiques, mais est également particulièrement utile pour la signalisation cohérente ou la synchronisation entre les threads.
  • sémaphore de comptage - pas de récursivité ni d'héritage, agit en tant que compteur de ressources cohérent à partir du nombre initial souhaité, les threads ne bloquent que le nombre net de ressources par rapport à la ressource.

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.

143
Tall Jeff

La réponse est pas 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 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écursif, cette impasse. Si vous les avez, ça fonctionne, mais ça peut casser. A :: foo () peut avoir laissé l'objet dans un état incohérent avant d'appeler bar (), en supposant que baz () ne puisse 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 d'utilisation des mutex: Le mutex protège un invariant. Lorsque le mutex est tenu, l'invariant peut changer, mais avant de relâcher le mutex, l'invariant est rétabli. Les verrous réentrants sont dangereux car la deuxième fois que vous obtenez 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 encore eu à résoudre un problème de ce type. Java a des verrous non réentrants ces jours-ci dans Java.util.concurrent.locks, en passant.

118
Jonathan

comme écrit par Dave Butenhof lui-même :

"Le plus gros problème de tous les gros problèmes avec les mutex récursifs est qu'ils vous incitent à perdre complètement le sens de votre schéma de verrouillage et de sa portée. C'est mortel. Mal. C'est le" mangeur de fils ". Vous gardez les verrous le plus rapidement possible. Toujours. Si vous appelez quelque chose avec un verrou en attente simplement parce que vous ne savez pas que l'appel est en attente, ou parce que vous ne savez pas si l'appelé a besoin du mutex, vous le tenez trop longtemps. pointer un fusil de chasse sur votre application et appuyer sur la gâchette. Vous avez probablement commencé à utiliser des threads pour obtenir une concurrence, mais vous venez de PRÉVENIR la concurrence. "

87
Chris Cleeland

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

Pourquoi êtes-vous sûr que c'est vraiment le bon modèle mental d'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 pose même dans les applications mono-thread et n’a rien de commun avec le multi-thread et les mutex.

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

14
Corpse

L'une des principales raisons pour lesquelles les mutex récursifs sont utiles est le fait d'accéder aux méthodes plusieurs fois par le même thread. Par exemple, supposons que si le verrouillage mutex protège une banque A/C contre le retrait, si des frais sont également associés à ce retrait, le même mutex doit être utilisé.

4
avis

Le seul cas d'utilisation valable pour le mutex de récurrence est lorsqu'un objet contient plusieurs méthodes. Lorsque l’une des méthodes modifie le contenu de l’objet, elle doit donc le verrouiller 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 à elle seule nécessite de verrouiller le mutex, le mutex récursif est gagnant-gagnant.

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

3
DarkZeros