Monitor.Wait/Pulse
n'est pas le seul moyen de faire attendre
un thread sur un événement se produisant dans un autre thread.
Les programmeurs Win32 sont habitués à utiliser divers autres mécanismes
depuis longtemps. Ces mécanismes sont proposés par les classes
AutoResetEvent
, ManualResetEvent
et
Mutex
qui dérivent toutes de WaitHandle
.
Toutes ces classes se trouvent dans le namespace System.Threading
.
Le mécanisme de Semaphore
de Win32 est intégré dans .NET 2.0 mais
par contre il n'a pas été intégré dans .NET 1.1. Si malgré tout vous en avez besoin,
vous pouvez l'intégrer vous même en utilisant P/Invoke, ou écrire votre propre
classe de comptage de sémaphores.
Même si cela peut surprendre, l'utilisation de ces classes peut être sensiblement
plus lente que l'utilisation des différentes méthodes de la classe Monitor
.
Je suppose que cela vient du fait que "sortir" du code managé pour faire un appel Win32 natif
puis ensuite revenir dans le code managé est plus couteux, comparé à la technique entièrement
managée que la classe Monitor
fournit.
La classe WaitHandle
ne propose que peu de méthodes/propriétés utiles :
WaitOne()
- utilisée pour attendre qu'un handle soit libéré/signalé.
Le comportement exact de cette méthode dépend de la classe concrète utilisée
(Mutex
, AutoResetEvent
ou ManualResetEvent
).
Close()/Dispose()
- sont utilisées pour libérer la ressource utilisée par le handle.
Handle
- est utilisée pour récupérer le handle natif qui a été encapsulé dans la classe
(la plupart des développeurs ne devrait pas avoir besoin d'utiliser cette méthode).
De plus, cette classe possède deux méthodes statiques permettant de manipuler
des ensembles de WaitHandle
:
WaitAny()
- utilisée pour attendre la libération/signalement
de n'importe quel handle dans une liste.
WaitAll()
- utilisée pour attendre la libération/signalement
de tous les handles d'une liste.
Toutes les méthodes WaitXXX()
sont surchargées pour vous permettre
de spécifier un timeout et demander de quitter ou pas le domaine de synchronisation
(la valeur par défaut pour ce dernier argument est "false").
La plupart des développeurs .NET n'auront pas besoin d'utiliser les domaines
de synchronisation. Malgré tout, si vous souhaitez en savoir plus, Richard Grimes
a écrit un article dessus pour le site Dr. Dobb's.
Ces deux classes de gestion d'événement (à ne pas confondre avec les événements .NET
qui n'ont rien à voir) sont très similaires. On peut
se les représenter comme des portes. Lorsqu'elles sont dans l'état "signalé" ou "set"
elles sont ouvertes. Lorsqu'elles sont dans l'état "non-signalé" ou "reset"
elles sont fermées. Un appel à WaitOne()
attend que la porte
s'ouvre afin que le thread puisse continuer. La différence entre les deux
classes est qu'une instance de AutoResetEvent
va se replacer elle-même
dans l'état "non-signalé" immédiatement après un appel à WaitOne()
(comme si chaque personne passant la porte la refermait derrière elle).
Avec une instance de ManualResetEvent
vous devez demander au thread
de se "reseter" (fermer la porte) lorsque vous voulez que les appels à WaitOne()
redeviennent bloquant. L'état signalé/non-signalé des deux classes peut être
manuellement modifié à tout moment et par n'importe quel thread en utilisant les
méthodes Set
et Reset
(ces deux méthodes retournent
un booleen indiquant si l'opération a réussi, mais la documentation ne donne
pas plus d'information sur les causes possibles d'un échec).
Voici un exemple qui simule une course avec 10 participants. Chaque thread
participant possède une instance de ManualResetEvent
qui, au départ, est à
l'état non-signalé. Quand un thread fini la course il passe en état signalé.
Le thread principal utilise la méthode WaitHandle.WaitAny
pour
attendre le premier participant finissant la course et utilise la valeur retournée
par la méthode pour déterminer qui est le gagnant. Ensuite le thread principal
utilise la méthode WaitHandle.WaitAll
pour attendre l'arrivée
des autres participants. Il est important de noter que si nous avions
utilisé des instances de AutoResetEvent
nous aurions du appeler Set
sur le gagnant car il aurait été automatiquement repassé à l'état non-signalé lors de l'appel
à WaitAny
et l'appel à WaitHandle.WaitAll
ne se serait jamais terminé.
using System; using System.Threading; class Test { static void Main() { ManualResetEvent[] events = new ManualResetEvent[10]; for (int i=0; i < events.Length; i++) { events[i] = new ManualResetEvent(false); Runner r = new Runner(events[i], i); new Thread(new ThreadStart(r.Run)).Start(); } int index = WaitHandle.WaitAny(events); Console.WriteLine ("***** The winner is {0} *****", index); WaitHandle.WaitAll(events); Console.WriteLine ("All finished!"); } } class Runner { static readonly object rngLock = new object(); static Random rng = new Random(); ManualResetEvent ev; int id; internal Runner (ManualResetEvent ev, int id) { this.ev = ev; this.id = id; } internal void Run() { for (int i=0; i < 10; i++) { int sleepTime; // La classe Random n'est pas garantie thread-safe... lock (rngLock) { sleepTime = rng.Next(2000); } Thread.Sleep(sleepTime); Console.WriteLine ("Runner {0} at stage {1}", id, i); } ev.Set(); } } |
Alors que les classes Auto/ManualResetEvent
ressemblent beaucoup
aux Monitor.Wait/Pulse
, la classe Mutex
s'apparente
plus à Monitor.Enter/Exit
. Un mutex possède un compteur
avec le nombre de fois qu'il a été acquis, et le thread propriétaire
courant. Si le compteur vaut zéro, le mutex n'a pas de propriétaire et il peut être
acquis par n'importe qui. Si le compteur ne vaut pas zéro, le propriétaire courant
peut encore l'acquérir autant de fois qu'il veut sans se bloquer, mais un tous
les autres thread doivent attendre que le compteur soit redescendu à zéro pour
pouvoir acquérir le mutex. Les méthodes WaitXXX()
sont utilisées
pour acquérir le mutex, et la méthode ReleaseMutex()
est utilisée
par le propriétaire pour décrémenter le compteur. Seul le propriétaire
du mutex peut faire diminuer le compteur.
Jusqu'à présent le Mutex
ressemble beaucoup au Monitor
.
La différence est qu'un Mutex
est un objet cross-process (un même
mutex peut être utilisé dans plusieurs processus) si vous lui donnez un nom.
Par exemple, un thread dans un processus peut attendre qu'un autre thread
dans un autre processus relâche le mutex. Quand vous construisez un mutex nommé
vous devez faire attention si oui ou non vous voulez/pouvez acquérir
sa propriété originelle.
Heureusement, il y a un constructeur qui permet de détecter si le systeme
a créé un mutex complètement nouveau ou s'il en a utilisé un existant.
Si le constructeur demande la propriété originelle du mutex, elle
ne lui sera accordée que s'il crée un nouveau mutex.
Il est recommandé de faire commencer les noms des mutex par "Local\" ou "Global\" afin d'indiquer s'ils doivent être créés dans l'espace de nom local ou global. Il me semble que l'espace de nom local est l'espace de nom par défaut, mais afin d'éviter tout risque il est préférable de le spécifier explicitement. Si vous créez un mutex dans l'espace de nom global, il sera partagé avec les autres utilisateurs loggés sur la machine. Au contraire, si vous créez un mutex dans l'espace de nom local, il est propre à l'utilisateur courant. Faites attention de choisir un nom plutôt original et rare afin de ne pas rentrer en collision avec d'autres programmes.
Pour être sincère, je pense que la principale utilisation des mutex
est la détection des autres instances de l'application déjà ouvertes.
La communication inter-processus
à ce niveau là n'est pas souvent utilisée. Le mutex vous permet aussi d'attendre
la libération de soit un, soit toute une liste de WaitHandles
. Pour les
autres utilisations où la classe Monitor
suffit, je conseille
d'utiliser cette dernière d'autant plus qu'en C# l'instruction lock
simplifie son utilisation. Voici un exemple détectant si une autre instance
de l'application est déjà ouverte :
using System; using System.Threading; class Test { static void Main() { bool firstInstance; using (Mutex mutex = new Mutex(true, @"Global\Jon.Skeet.MutexTestApp", out firstInstance)) { if (!firstInstance) { Console.WriteLine ("Other instance detected; aborting."); return; } Console.WriteLine ("We're the only instance running - yay!"); for (int i=0; i < 10; i++) { Console.WriteLine (i); Thread.Sleep(1000); } } } } |
Si vous faites tourner cet exemple dans deux consoles différentes, l'une va compter
jusqu'à 10 doucement pendant que l'autre s'arrêtera après avoir détecté qu'une autre
instance de l'application est en train de tourner. L'instruction using
autour du mutex doit être étendue autour de l'ensemble du code exécuté, sinon, une autre
instance pourrait créer un nouveau mutex avec le même nom une fois que le précédent mutex
a été détruit. Par exemple, supposons que vous utilisez une variable local sans
instruction using
comme ci-dessous :
using System; using System.Threading; class Test { static void Main() { bool firstInstance; // Code a ne pas utiliser Mutex mutex = new Mutex(true, @"Global\Jon.Skeet.MutexTestApp", out firstInstance); if (!firstInstance) { Console.WriteLine ("Other instance detected; aborting."); return; } Console.WriteLine ("We're the only instance running - yay!"); for (int i=0; i < 10; i++) { Console.WriteLine (i); Thread.Sleep(1000); } } } |
En exécutant le code ci-dessus en mode debug (ou le Garbage Collector
est très conservateur), vous trouverez probablement que tout fonctionne correctement.
En dehors du mode debug, le Garbage Collector peut repérer que la variable mutex
n'est plus utilisée après son initialisation, il peut alors collecter
la variable à tout moment et détruire le mutex. L'instruction using
utilisée précédemment est seulement l'une des techniques pour contourner ce problème.
Vous pouvez aussi déclarer la variable statique ou utiliser GC.KeepAlive(mutex);
à la fin de la méthode pour être sûr que le Garbage Collector ne supprime pas la variable.
Lire la version originale de cette page
Revenir sur la page d'accueil de sylvain114