En C#, les mécanismes Monitor, Mutex et Interlocked permettent de synchroniser les threads. Leur fonctionnement et leurs principes sous-jacents diffèrent, les rendant adaptés à divers scénarios.
- Monitor (Moniteur)
1.1 Rôle
Le Monitor est le mécanisme de synchronisation de threads le plus courant dans .NET. Il fournit une fonctionnalité de verrouillage mutuel basée sur les objets. Le mot-clé lock est en réalité une simplification syntaxique pour les appels au Monitor.
1.2 Principe de fonctionnement
Implémentation basée sur le SyncBlock :
// La traduction compilée du mot-clé lock
lock (objet)
{
// Code de la section critique
}
// Est en réalité compilé comme :
bool verrouAcquis = false;
try
{
Monitor.Enter(objet, ref verrouAcquis);
// Code de la section critique
}
finally
{
if (verrouAcquis)
Monitor.Exit(objet);
}
Mécanisme du SyncBlock :
- Chaque objet possède un champ SyncBlockIndex.
- À la première utilisation du Monitor, le CLR alloue un SyncBlock.
- Le SyncBlock contient l'état du verrou, le thread propriétaire, un compteur de récursivité, une file d'attente, etc.
1.3 Méthodes clés
using System;
using System.Threading;
class ExempleMonitor
{
private static readonly object _verrou = new object();
private static bool _conditionPrête = false;
static void Main()
{
Thread fil1 = new Thread(Travailleur1);
Thread fil2 = new Thread(Travailleur2);
fil1.Start();
fil2.Start();
fil1.Join();
fil2.Join();
}
static void Travailleur1()
{
Monitor.Enter(_verrou);
try
{
Console.WriteLine("Travailleur1 a obtenu le verrou");
Thread.Sleep(1000);
_conditionPrête = true;
Monitor.Pulse(_verrou); // Réveille un thread en attente
}
finally
{
Monitor.Exit(_verrou);
}
}
static void Travailleur2()
{
Monitor.Enter(_verrou);
try
{
Console.WriteLine("Travailleur2 a obtenu le verrou, vérifie la condition");
while (!_conditionPrête)
{
Console.WriteLine("Travailleur2 attend que la condition soit remplie");
Monitor.Wait(_verrou); // Libère le verrou et attend
}
Console.WriteLine("Condition remplie, Travailleur2 continue");
}
finally
{
Monitor.Exit(_verrou);
}
}
}
1.4 Avantages
-
Bonnes performances : Plus léger que le Mutex, opérations en mode utilisateur.
-
Variables de condition : Le mécanisme Wait/Pulse permet des modes de synchronisation complexes.
-
Verrou récursif : Un même thread peut acquérir le verrou plusieurs fois.
-
Intégration au langage : Le mot-clé
lockoffre une syntaxe concise. -
Interlocked (Opérations atomiques)
2.1 Rôle
La classe Interlocked fournit un ensemble de méthodes d'opérations atomiques pour lire et écrire des variables partagées de manière sûre dans un environnement multithread, sans utiliser de verrouillage.
2.2 Méthodes courantes
using System.Threading;
// Incrémentation / Décrémentation atomiques
int compteur = 0;
Interlocked.Increment(ref compteur); // compteur++
Interlocked.Decrement(ref compteur); // compteur--
// Addition atomique
Interlocked.Add(ref compteur, 5); // compteur += 5
// Échange atomique
int ancienneValeur = Interlocked.Exchange(ref compteur, 10); // compteur = 10, retourne l'ancienne valeur
// Compare et échange (CAS)
int comparant = 5;
int nouvelleValeur = 10;
bool reussite = Interlocked.CompareExchange(ref compteur, nouvelleValeur, comparant) == comparant;
// Si compteur == comparant, alors compteur = nouvelleValeur
// Lecture atomique d'une valeur 64 bits (sur système 32 bits)
long longValue = 0;
long resultat = Interlocked.Read(ref longValue);
2.3 Principe de fonctionnement
Niveau instruction CPU atomique
- Basé sur les instructions processeur avec préfixe LOCK.
- Verrouille le bus mémoire pendant l'exécution pour garantir l'atomicité.
- Barrières mémoire (Memory Barrier) au niveau matériel.
Garanties du modèle mémoire :
// Pseudo-code illustrant le principe de Interlocked.Increment
LOCK INC [adresse_mémoire] // Instruction CPU atomique
2.4 Avantages
- Haute performance : 10 à 100 fois plus rapide qu'un verrou.
- Non bloquant : Ne cause pas de mise en attente des threads.
- Évite les interblocages : Pas d'acquisition/libération de verrou.
- Léger : Minimise les frais généraux système.
2.5 Exemple
using System;
using System.Threading;
using System.Threading.Tasks;
class ExempleInterlocked
{
private static int _compteur = 0;
static void Main()
{
var taches = new Task[10];
for (int i = 0; i < 10; i++)
{
taches[i] = Task.Run(() => IncrementerCompteur());
}
Task.WaitAll(taches);
Console.WriteLine($"Compteur final : {_compteur}"); // Toujours 1000
}
static void IncrementerCompteur()
{
for (int i = 0; i < 100; i++)
{
Interlocked.Increment(ref _compteur);
}
}
}
- Mutex (Verrou mutuel)
3.1 Rôle
Le Mutex est un primitif de synchronisation au niveau système, utilisé pour protéger des ressources partagées et garantir qu'un seul thread à la fois peut accéder à une section de code protégée.
3.2 Utilisation de base
using System;
using System.Threading;
class ExempleMutex
{
private static Mutex _mutex = new Mutex();
private static int _ressourcePartagee = 0;
static void Main()
{
var threads = new Thread[5];
for (int i = 0; i < 5; i++)
{
threads[i] = new Thread(Travailleur);
threads[i].Start(i);
}
foreach (var thread in threads)
{
thread.Join();
}
Console.WriteLine($"Résultat final : {_ressourcePartagee}");
}
static void Travailleur(object id)
{
for (int i = 0; i < 100; i++)
{
_mutex.WaitOne(); // Acquiert le verrou
try
{
_ressourcePartagee++;
}
finally
{
_mutex.ReleaseMutex(); // Libère le verrou
}
}
}
}
3.3 Principe de fonctionnement
Le Mutex est basé sur un mécanisme de synchronisation au niveau du noyau du système d'exploitation. Lorsqu'un thread demande un Mutex, si celui-ci n'est pas détenu, le thread le reçoit ; s'il est déjà détenu, le thread est mis en attente jusqu'à ce que le thread propriétaire le libère. Le système d'exploitation gère la mise en atente et le réveil des threads.
3.4 Mutex inter-processus
using System;
using System.Threading;
class MutexInterProcessus
{
static void Main()
{
using (var mutex = new Mutex(false, "Global\\MonApplicationMutex"))
{
try
{
if (mutex.WaitOne(5000))
{
try
{
Console.WriteLine("Verrou inter-processus obtenu, opération critique...");
Thread.Sleep(3000);
}
finally
{
mutex.ReleaseMutex();
}
}
else
{
Console.WriteLine("Timeout lors de l'acquisition du verrou.");
}
}
catch (AbandonedMutexException)
{
Console.WriteLine("Mutex abandonné détecté, nettoyage.");
}
}
}
}
3.5 Avantages et inconvénients
Avantages :
- Synchronisation inter-processus : Peut être utilisé entre différents processus.
- Gestion par le système d'exploitation : Fiable, géré par le noyau.
- Contrôle de timeout : Possibilité de définir un délai d'attente.
- Support récursif : Un même thread peut l'acquérir plusieurs fois.
Inconvénients :
-
Frais généraux importants : Transition entre mode utilisaetur et mode noyau.
-
Plus lourd : Moins adapté pour les verrous à grain fin nécessitant de hautes performances.
-
Comparaison
| Caractéristique | Monitor | Mutex | Interlocked |
|---|---|---|---|
| Performence | Bonne | Faible | Excellente |
| Périmètre | Intra-processus | Inter-processus | Intra-processus |
| Blocage | Attente utilisateur + noyau | Attente noyau | Non bloquant |
| Variables de condition | Oui (Wait/Pulse) | Non | Sans objet |
| Usage typique | Synchronisation générale | Synchronisation inter-processus/complexe | Compteurs/Drapeaux |
Bonnes pratiques
- Privilégier la solution la plus simple : Utiliser
lock(Monitor) si possible. - Scénarios sensibles aux performances : Considérer Interlocked ou des structures de données sans verrou.
- Besoin inter-processus : Nécessite Mutex ou Semaphore.
- Éviter la synchronisation excessive : Synchroniser uniquement quand nécessaire.
- Attention aux interblocages : Acquérir plusieurs verrous dans un ordre fixe.