1. Introduction
Rappelons l'exemple de la leçon précédente : deux threads dans notre application encore simple incrémentent un compteur partagé, mais la valeur finale ne correspond pas toujours à ce qui est attendu. Pour protéger ce compteur nous avons déjà utilisé le mot-clé lock (ou, pour être précis, Monitor), qui convient pour la synchronisation seulement à l'intérieur d'un même processus. Mais que faire si votre programme n'est pas le seul à vouloir utiliser la ressource ? Par exemple, vous écrivez un super service lancé en deux instances, et les deux veulent écrire dans le même fichier... ou contrôler un équipement, comme le port d'une imprimante ? Là intervient le bon vieux mutex (de mutual exclusion — « exclusion mutuelle »).
Concept
Un mutex est un primitive de synchronisation qui non seulement limite l'accès à une ressource entre threads dans un seul processus, mais permet aussi de coordonner l'accès entre différents processus sur la même machine. Imaginez-le comme une grosse pancarte « Occupé » devant une salle de réunion, visible à la fois par les employés et les visiteurs.
En .NET, la classe prévue pour ça est System.Threading.Mutex.
Quand Mutex est vraiment nécessaire :
- Quand vous synchronisez l'accès entre threads dans différents processus (par exemple, deux applications séparées travaillent sur le même fichier).
- Quand la ressource est si précieuse et indivisible que même le contexte du processus ne peut pas partager les droits d'accès.
Pour la synchronisation uniquement entre threads d'un même processus on utilise généralement lock (Monitor). Mutex est préférable pour la synchronisation inter-processus, parce qu'il est plus lourd et plus lent.
Comment fonctionne Mutex
flowchart TD
A(Processus 1) --|Demande|--> M(Mutex)
B(Processus 2) --|Demande|--> M
M --|Autorise un seul|--> R(Ressource partagée)
A --|Libère|--> M
B --|Après libération|--> M
2. Syntaxe de base pour travailler avec Mutex
Création
Un Mutex se crée aussi simplement que la plupart des autres classes de synchronisation :
using System.Threading;
Mutex mutex = new Mutex();
Méthodes principales
- WaitOne() — tentative de prendre le mutex ; si ça échoue, le thread est bloqué jusqu'à ce qu'un autre libère le mutex.
- ReleaseMutex() — libère le mutex, permettant à d'autres threads ou processus d'entrer en section critique.
Exemple simple : synchronisation entre threads dans un seul processus
using System;
using System.Threading;
class Program
{
static Mutex mutex = new Mutex();
static void Main()
{
Thread t1 = new Thread(PrintNumbers);
Thread t2 = new Thread(PrintNumbers);
t1.Start();
t2.Start();
t1.Join();
t2.Join();
}
static void PrintNumbers()
{
for (int i = 0; i < 5; i++)
{
mutex.WaitOne(); // Entrer en section critique
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}: {i}");
mutex.ReleaseMutex(); // Sortir de la section critique
Thread.Sleep(100); // Pour la démonstration
}
}
}
Dans cet exemple, les deux threads accèdent tour à tour à la console pour imprimer.
3. Synchronisation inter-processus avec un mutex nommé
Pour la vraie « grosse artillerie » — la synchronisation entre processus — on utilise un mutex nommé. Vous lui donnez un nom, et tous les processus sur la machine peuvent s'y référer.
Mutex mutex = new Mutex(false, "MyApp_Mutex");
Paramètres du constructeur :
- Premier paramètre (bool initiallyOwned) — est-ce que le thread veut prendre le mutex immédiatement après la création. En général — false.
- Deuxième paramètre — le nom du mutex. Tous les processus qui utilisent un mutex avec ce nom accèdent au même objet, par exemple "MyApp_Mutex".
Exemple : deux applications partageant un Mutex
Lancez le même programme dans deux fenêtres différentes pour voir l'effet.
using System;
using System.Threading;
class Program
{
static void Main()
{
using (Mutex mutex = new Mutex(false, "MySuperUniqueMutexName"))
{
Console.WriteLine("Tentative d'entrer en section critique...");
mutex.WaitOne(); // On attend que l'autre processus libère le mutex
try
{
Console.WriteLine("Section critique occupée par ce processus.");
Console.WriteLine("Appuyez sur Enter pour sortir de la section critique.");
Console.ReadLine();
}
finally
{
mutex.ReleaseMutex();
Console.WriteLine("Section critique libérée.");
}
}
}
}
Essayez :
- Ouvrez deux fenêtres avec cette application.
- Lancez les deux — le deuxième attendra que vous appuyiez sur Enter dans le premier.
4. Lancer l'application une seule fois
Mutex est souvent utilisé pour limiter le nombre d'instances en cours d'exécution. Par exemple : « Hé, utilisateur, la prochaine fois n'ouvre pas deux copies de la calculette ! »
using System;
using System.Threading;
class Program
{
static void Main()
{
bool createdNew;
using (Mutex mutex = new Mutex(true, "CalculatorAppInstanceMutex", out createdNew))
{
if (!createdNew)
{
Console.WriteLine("L'application est déjà lancée !");
return;
}
Console.WriteLine("L'application a démarré avec succès. Appuyez sur Enter pour quitter.");
Console.ReadLine();
}
}
}
Ce pattern se rencontre souvent dans les applications desktop : la première instance démarre, la seconde se contente d'afficher un message et se termine. Un exemple officiel de Microsoft est dans la documentation.
5. Erreurs typiques en travaillant avec Mutex
Erreur №1 : oubli de appeler ReleaseMutex().
Si un thread a pris le mutex (WaitOne()) mais n'a pas appelé ReleaseMutex() (crash avec exception ou simple oubli), aucun autre thread ou processus ne pourra entrer tant que le thread « oublieux » ne se termine pas. Ça peut causer un deadlock. Bon style : toujours utiliser try-finally :
mutex.WaitOne();
try
{
// section critique
}
finally
{
mutex.ReleaseMutex();
}
Erreur №2 : nombre incorrect d'appels à ReleaseMutex().
Si vous appelez ReleaseMutex() plus de fois que vous avez appelé WaitOne(), une exception ApplicationException sera levée.
Erreur №3 : tentative de libérer un Mutex qui n'est pas le vôtre.
Le mutex est « lié » au thread qui l'a pris. Seul ce thread a le droit d'appeler ReleaseMutex(). Si un autre thread essaie de le libérer, .NET lèvera une exception.
Erreur №4 : utiliser Mutex là où lock suffit.
Mutex est plus lent que le simple lock, puisqu'il peut fonctionner entre processus et requiert des appels système. Recommandation : si vous n'avez pas besoin de synchronisation inter-processus — utilisez lock.
Erreur №5 : mauvais nommage du mutex.
Si vous choisissez un nom trop simple (par exemple, "MyMutex"), vous pouvez accidentellement « partager » le mutex avec un autre programme qui utilise le même nom. Mieux vaut utiliser des noms uniques (par exemple incluant le nom de votre entreprise ou application).
6. Détails utiles
WaitOne(timeout)
On peut préciser un timeout pour l'attente du mutex :
if (mutex.WaitOne(5000)) // on attend jusqu'à 5 secondes
{
try { /* ... */ }
finally { mutex.ReleaseMutex(); }
}
else
{
Console.WriteLine("Impossible d'accéder à la ressource en 5 secondes !");
}
Mutex.TryOpenExisting
Si vous devez vous connecter à un mutex déjà existant, utilisez la méthode statique :
if (Mutex.TryOpenExisting("DiaryFileWriteMutex", out Mutex existingMutex))
{
// maintenant existingMutex est une référence au mutex existant
}
Séparation entre utilisateurs
Par défaut, un mutex nommé est accessible à tous les utilisateurs ayant les droits de créer des objets système. Si vous avez besoin d'un contrôle d'accès plus strict — utilisez le constructeur avec MutexSecurity.
Tableau comparatif : lock/Monitor, Mutex, Semaphore
| Primitif | Synchronisation inter-processus ? | Vitesse | Où utiliser |
|---|---|---|---|
|
Non | Très élevée | Entre threads dans un même processus |
|
Oui | Plus lente (coûteuse) | Entre threads de processus différents |
|
Oui (avec NamedSemaphore) | Comparable à Mutex | Quand il faut limiter le nombre de threads |
Plus de détails sur les semaphores — dans la prochaine leçon.
États de Mutex
stateDiagram-v2
[*] --> Unowned
Unowned --> Owned : WaitOne()
Owned --> Owned : WaitOne() (reentrant)
Owned --> Unowned : ReleaseMutex()
Owned --> Abandoned : thread "mort", n'ayant pas appelé ReleaseMutex()
Abandoned --> Unowned
- Unowned — personne ne possède le mutex.
- Owned — le mutex est pris par un thread.
- Abandoned — un thread s'est terminé brusquement sans appeler ReleaseMutex(). Le prochain qui obtiendra le mutex recevra une exception AbandonedMutexException (c'est un signe d'erreur et d'un état potentiellement incohérent de la ressource).
GO TO FULL VERSION