J’ai le code suivant:
using (Mutex mut = new Mutex(false, MUTEX_NAME)) { if (mut.WaitOne(new TimeSpan(0, 0, 30))) { // Some code that deals with a specific TCP port // Don't want this to run at the same time in another process } }
J’ai défini un point d’arrêt dans le bloc if
et exécuté le même code dans une autre instance de Visual Studio. Comme prévu, l’appel de .WaitOne
bloque. Cependant, à ma grande surprise, dès que je continue dans la première instance et que le bloc d’ using
termine, je reçois une exception dans le second processus concernant un Mutex abandonné.
La solution consiste à appeler ReleaseMutex
:
using (Mutex mut = new Mutex(false, MUTEX_NAME)) { if (mut.WaitOne(new TimeSpan(0, 0, 30))) { // Some code that deals with a specific TCP port // Don't want this to run twice in multiple processes } mut.ReleaseMutex(); }
Maintenant, les choses fonctionnent comme prévu.
Ma question: D’habitude, le but d’un IDisposable
est de nettoyer l’ état dans lequel vous mettez les choses. Je pourrais peut-être avoir plusieurs attentes et versions dans un bloc, mais quand le handle du Mutex est éliminé automatiquement? En d’autres termes, pourquoi dois-je appeler ReleaseMutex
si je suis dans un bloc d’ using
?
Je suis également préoccupé par le fait que si le code dans le bloc if
bloque, j’aurai abandonné les mutexes qui traînent.
Y a-t-il un avantage à placer Mutex
dans un bloc d’ using
? Ou, devrais-je simplement créer une instance de Mutex
, l’envelopper dans un try / catch et appeler ReleaseMutex()
dans le bloc finally (implémenter exactement ce que je pensais que Dispose()
pourrait faire)
La documentation explique (dans la section “Remarques”) qu’il existe une différence conceptuelle entre l’ instanciation d’ un object Mutex (qui, en fait, ne fait rien de particulier en ce qui concerne la synchronisation) et l’ acquisition d’ un Mutex (en utilisant WaitOne
). Notez que:
WaitOne
renvoie un booléen, ce qui signifie que l’acquisition d’un Mutex peut échouer (timeout) et que les deux cas doivent être traités WaitOne
renvoie true
, le thread appelant a acquis le Mutex et doit appeler ReleaseMutex
, sinon Mutex sera abandonné false
, le thread appelant ne doit pas appeler ReleaseMutex
Donc, Mutexes a plus à faire que l’instanciation. Quant à savoir si vous devez utiliser de toute façon, jetons un coup d’œil à ce que fait Dispose
(comme hérité de WaitHandle
):
protected virtual void Dispose(bool explicitDisposing) { if (this.safeWaitHandle != null) { this.safeWaitHandle.Close(); } }
Comme nous pouvons le voir, le Mutex n’est pas sorti, mais il y a un certain nettoyage à effectuer, donc restr fidèle à l’ using
serait une bonne approche.
En ce qui concerne la procédure à suivre, vous pouvez bien entendu utiliser un bloc try/finally
pour vous assurer que, si le Mutex est acquis, il sera correctement publié. C’est probablement l’approche la plus simple.
Si vous ne vous souciez pas vraiment du cas où le Mutex ne soit pas acquis (ce que vous n’avez pas indiqué, puisque vous transmettez un TimeSpan
à WaitOne
), vous pouvez WaitOne
Mutex
dans votre propre classe qui implémente IDisposable
, acquérir le Mutex dans le constructeur (en utilisant WaitOne()
sans arguments), et le relâche dans Dispose
. Bien que, je ne le recommanderais probablement pas, car cela provoquerait une attente indéfinie de vos threads si quelque chose ne va pas, et quelles que soient les bonnes raisons de traiter explicitement les deux cas lors d’une tentative d’acquisition, comme indiqué par @HansPassant.
Cette décision de conception a été prise il y a très longtemps. Il y a plus de 21 ans, bien avant que .NET n’ait jamais été envisagé ou que la sémantique d’IDisposable n’ait été envisagée. La classe .NET Mutex est une classe wrapper pour la prise en charge du système d’exploitation sous-jacent pour les mutex. Le constructeur identifie CreateMutex , la méthode WaitOne () appelle WaitForSingleObject () .
Notez la valeur de retour WAIT_ABANDONED de WaitForSingleObject (), celle qui génère l’exception.
Les concepteurs de Windows ont mis en place la règle rigoureuse qu’un thread qui possède le mutex doit appeler ReleaseMutex () avant de quitter. Et si ce n’est pas le cas, c’est une indication très forte que le thread s’est terminé de manière inattendue, généralement par le biais d’une exception. Ce qui implique que la synchronisation est perdue, un bogue très sérieux. Comparez à Thread.Abort (), un moyen très dangereux de terminer un thread dans .NET pour la même raison.
Les concepteurs .NET n’ont en aucun cas modifié ce comportement. Pas du tout parce qu’il n’y a aucun moyen de tester l’état du mutex autrement qu’en effectuant une attente. Vous devez appeler ReleaseMutex (). Et notez que votre deuxième extrait n’est pas correct non plus; vous ne pouvez pas l’appeler sur un mutex que vous n’avez pas acquis. Il doit être déplacé à l’intérieur du corps de l’instruction if ().
L’une des utilisations principales d’un mutex est de s’assurer que le seul code qui verra un object partagé dans un état qui ne satisfait pas ses invariants est le code qui (espérons-le temporairement) place cet état dans cet état. Un modèle normal pour le code qui doit modifier un object est:
Si quelque chose se passe mal après le début de # 2 et avant que # 3 soit terminé, l’object peut être laissé dans un état qui ne satisfait pas ses invariants. Comme le modèle approprié est de libérer un mutex avant de le supprimer, le fait que le code dispose d’un mutex sans le libérer implique que quelque chose s’est mal passé quelque part. En tant que tel, il n’est peut-être pas sûr que le code entre dans le mutex (puisqu’il n’a pas été publié), mais il n’y a aucune raison d’attendre que le mutex soit publié (puisqu’il a été jeté – il ne le sera jamais) . Ainsi, la procédure appropriée consiste à lancer une exception.
Un modèle qui est un peu plus joli que celui implémenté par l’object .NET mutex consiste à renvoyer à la méthode “acquisition” un object IDisposable
qui encapsule non pas le mutex, mais plutôt une acquisition particulière de celui-ci. La disposition de cet object libère alors le mutex. Le code peut alors ressembler à:
using(acq = myMutex.Acquire()) { ... stuff that examines but doesn't modify the guarded resource acq.EnterDanger(); ... actions which might invalidate the guarded resource ... actions which make it valid again acq.LeaveDanger(); ... possibly more stuff that examines but doesn't modify the resource }
Si le code interne échoue entre EnterDanger
et LeaveDanger
, l’object d’acquisition doit invalider le mutex en appelant Dispose
sur celui-ci, car la ressource protégée peut être dans un état corrompu. Si le code interne échoue ailleurs, le mutex doit être libéré puisque la ressource protégée est dans un état valide et que le code dans le bloc using
n’a plus besoin d’y accéder. Je n’ai aucune recommandation particulière de bibliothèques implémentant ce modèle, mais il n’est pas particulièrement difficile à mettre en œuvre en tant que wrapper autour d’autres types de mutex.
Ok, poster une réponse à ma propre question. D’après ce que je peux dire, c’est le moyen idéal pour implémenter un Mutex
qui:
WaitOne
ssi WaitOne
a réussi. J’espère que cela aide quelqu’un!
using (Mutex mut = new Mutex(false, MUTEX_NAME)) { if (mut.WaitOne(new TimeSpan(0, 0, 30))) { try { // Some code that deals with a specific TCP port // Don't want this to run twice in multiple processes } catch(Exception) { // Handle exceptions and clean up state } finally { mut.ReleaseMutex(); } } }
Mise à jour: Certains peuvent prétendre que si le code dans le bloc try
met votre ressource dans un état instable, vous ne devez pas le libérer et le laisser à la place. En d’autres termes, appelez simplement mut.ReleaseMutex();
lorsque le code se termine avec succès, et ne le placez pas dans le bloc finally
. Le code acquérant le Mutex pourrait alors capturer cette exception et faire la bonne chose .
Dans ma situation, je ne change pas vraiment d’état. J’utilise temporairement un port TCP et je ne peux pas exécuter une autre instance du programme en même temps. Pour cette raison, je pense que ma solution ci-dessus est bien mais la vôtre peut être différente.
Nous devons mieux comprendre que .net pour savoir ce qui se passe au début de la page MSDN donne le premier indice que quelqu’un “bizarre” se passe:
Une primitive de synchronisation pouvant également être utilisée pour la synchronisation interprocessus .
Un Mutex est un « object nommé » Win32 , chaque processus le verrouille par son nom , l’object .net n’est qu’une enveloppe autour des appels Win32. Muxtex lui-même réside dans l’espace d’adressage Windows Kernal et non dans l’espace d’adressage de votre application.
Dans la plupart des cas, il est préférable d’utiliser un moniteur si vous essayez uniquement de synchroniser l’access aux objects au sein d’un même processus.
Si vous avez besoin de garantir que le mutex est libéré, basculez sur un bouton try final, puis mettez la version mutex dans le bloc finally. On suppose que vous possédez et avez une poignée pour le mutex. Cette logique doit être incluse avant que la version soit appelée.
En lisant la documentation de ReleaseMutex
, il semble que la décision de conception était que Mutex soit libéré consciemment. si ReleaseMutex
n’est pas appelé, cela signifie une sortie anormale de la section protégée. mettre le relâchement dans un dernier ou éliminer, contourne ce mécanisme. vous êtes toujours libre d’ignorer l’exception AbandonedMutexException.
Sachez que Mutex.Dispose () exécuté par le Garbage Collector échoue car le processus de nettoyage de la mémoire ne possède pas le descripteur selon Windows.
Dispose
dépend de WaitHandle
pour être libéré. Donc, même si vous using
appels Dispose
, cela n’affectera pas jusqu’à ce que les conditions de l’état stable soient remplies. Lorsque vous appelez ReleaseMutex
vous ReleaseMutex
au système que vous publiez la ressource et que, par conséquent, vous pouvez vous en débarrasser.