CodeGym /Cours /C# SELF /Verrous : lock et la classe Monitor

Verrous : lock et la classe Monitor

C# SELF
Niveau 56, Leçon 1
Disponible

1. Introduction

Regardons une situation familière quand on bosse avec des threads. Supposons qu'on ait un compteur commun de succès dans une application très simple.

int counter = 0;

void IncrementCounter()
{
    for (int i = 0; i < 100_000; i++)
    {
        counter++; // Pas atomique !
    }
}

// On lance deux threads :
Thread t1 = new Thread(IncrementCounter);
Thread t2 = new Thread(IncrementCounter);

t1.Start();
t2.Start();

t1.Join();
t2.Join();

Console.WriteLine($"Counter: {counter}");

Exécutez ce code plusieurs fois. Vous ne verrez presque jamais 200_000 ! Pourquoi ? Les deux threads se gênent constamment, parfois les deux lisent la variable en même temps, l'incrémentent — et écrivent le même résultat. En bout de course, une partie des incréments "se perd".

C'est la condition de course, ou Race Condition. Sans règles de "tour", les threads se battent littéralement pour les données.

Section critique : qu'est-ce que c'est ?

Section critique — c'est un morceau de code qui ne doit être exécuté que par un seul thread à la fois. Pour revenir à notre analogie de cuisine : c'est comme un robinet — si deux personnes essaient de se laver au-dessus du même lavabo, de l'eau et du dentifrice seront partout. Convenons d'entrer dans la salle de bain un par un !

Dans notre exemple la section critique est l'instruction counter++.

2. Le mot-clé lock

En C# il y a un moyen concis et sûr de créer une section critique — le mot-clé lock. Il nous cache la gestion complexe du primitif de synchronisation et veille à ce qu'un seul thread entre dans le bloc protégé à la fois.

Comment utiliser lock

Synthaxe :

lock (lockerObject)
{
    // Code qui ne peut être exécuté que par un seul thread à la fois
}

lockerObject — c'est n'importe quel objet qui existe pendant toute la durée du programme. Habituellement on fait comme ça :

private static object locker = new object();

Attention : n'utilisez jamais pour ça des chaînes, des nombres ou des objets accessibles par d'autres de manière imprévisible ! Utilisez seulement des objets privés que vous savez ne pas réutiliser ailleurs.

Corrigeons notre exemple

private static object locker = new object();
int counter = 0;

void IncrementCounter()
{
    for (int i = 0; i < 100_000; i++)
    {
        lock (locker)
        {
            counter++; // Maintenant c'est atomique !
        }
    }
}

Maintenant deux ou dix threads entreront dans ce bout de code à tour de rôle. Le résultat sera un parfait 200_000. Les chatons sont contents !

3. Comment lock fonctionne en interne ? La classe Monitor

En interne le mot-clé lock utilise la classe System.Threading.Monitor. C'est un vrai secrétaire qui laisse entrer seulement avec un badge spécial.

La syntaxe équivalente à lock (mais plus "déshabillée") :

Monitor.Enter(locker);
try
{
    // Section critique
}
finally
{
    Monitor.Exit(locker);
}

La différence clé — vous êtes obligé de garantir vous-même que Monitor.Exit sera appelé. Habituellement on utilise try...finally pour ça. Si vous oubliez d'appeler Exit(), le thread restera "à l'intérieur" pour toujours, et les autres threads attendront indéfiniment — le programme se bloquera comme un vieux Windows pendant une mise à jour.

Tableau : lock vs. manuel Monitor

Méthode Sécurité contre les erreurs Facilité d'écriture Flexibilité
lock(obj)
Oui Oui Non
Monitor
Seulement avec try/finally Non Oui

Dans 99% des cas utilisez lock. Le Monitor manuel est utile seulement si vous avez besoin de flexibilité maximale : par exemple, si vous voulez faire une méthode de verrouillage avec timeout.

4. Arguments pour lock : ce qu'on peut et ne peut pas faire

Erreur très fréquente chez les débutants : utiliser une chaîne ou un autre objet "visible" pour le verrou. Par exemple :

lock ("mylock") { /*...*/ } // Très mauvais !

Le problème est que les chaînes sont internées (uniques pour toute l'application), vous pouvez facilement entrer en conflit avec des bibliothèques tierces et aboutir à un programme "mort". Utilisez toujours des objets privés :

private readonly object myLock = new object();

lock (myLock)
{
    // seul votre code connaît myLock
}

5. lock : exemple avec affichage console

On va s'entraîner sérieusement ! Créons une mini-application où deux threads impriment des lignes, mais l'accès à la console est aussi synchronisé — pour que le texte ne soit pas mélangé.

private static object consoleLock = new object();

void PrintMessages(string name)
{
    for (int i = 0; i < 5; i++)
    {
        lock (consoleLock)
        {
            Console.WriteLine($"{name}: Message {i + 1}");
            Thread.Sleep(50); // On simule du traitement
        }
    }
}

Thread t1 = new Thread(() => PrintMessages("Thread 1"));
Thread t2 = new Thread(() => PrintMessages("Thread 2"));

t1.Start();
t2.Start();

t1.Join();
t2.Join();

Résultat : les lignes s'affichent proprement les unes après les autres, pas de mélange. Cette approche est souvent utilisée pour le logging, pour éviter des "caractères bizarres" dans les logs.

6. Détails utiles

Contrôle manuel du verrou : Monitor avancé

Quand le lock standard ne suffit pas (par exemple si vous voulez essayer d'entrer dans la section sans attendre indéfiniment), vous pouvez utiliser la méthode Monitor.TryEnter.

if (Monitor.TryEnter(locker, 100)) // attente 100 ms
{
    try
    {
        // Section critique
    }
    finally
    {
        Monitor.Exit(locker);
    }
}
else
{
    Console.WriteLine("Impossible d'obtenir le verrou en 100 millisecondes");
}

C'est pratique si votre programme ne veut pas "se bloquer" — par exemple, on peut afficher un message à l'utilisateur ou faire quelque chose d'utile pendant que la ressource partagée est occupée.

Visualisation : comment fonctionne le verrou (schéma)

flowchart LR
    A[Thread 1 : veut entrer dans la section critique]
    B[Thread 2 : veut entrer dans la section critique]
    C[locker libre]
    D[Thread 1 exécute le code à l'intérieur du lock]
    E[Thread 2 attend]
    F[Thread 1 est sorti du lock]
    G[Thread 2 obtient l'accès]
    
    A -- Vérifie locker --> C
    C -- locker libre --> D
    B -- Vérifie locker --> D
    D -- lock occupé --> E
    D -- Terminé --> F
    F -- locker libéré --> G
    E -- locker maintenant libre --> G

Verrous et performance

Les verrous fonctionnent simplement : un seul thread à la fois peut exécuter le bout de code entre les accolades. C'est excellent pour l'intégrité des données, mais... plus il y a de threads "en file", plus tout devient lent. La synchronisation n'est donc pas une panacée : essayez de garder les sections critiques aussi petites que possible.

Astuce : si l'exécution de la section critique prend des fractions de millisecondes — tout va bien. Si c'est un calcul long, des I/O, du réseau ou des fichiers — mieux vaut les sortir du bloc lock. D'abord lisez/calculer, puis mettez à jour rapidement la valeur partagée à l'intérieur de la protection.

En entretien et en pratique

Dans n'importe quelle grosse application qui utilise des threads, les recruteurs demanderont forcément : "Que faire si deux threads accèdent à la même variable ?" Montrez du code avec un verrou — et votre CV ne disparaîtra pas dans la boîte noire de l'automatisation RH.

En pratique, surtout dans les systèmes à haute charge, on utilise des mécanismes de synchronisation plus avancés — mais lock et Monitor restent des standards d'or pour les cas simples.

7. Particularités d'utilisation des verrous et erreurs typiques

L'erreur la plus fréquente — "oublier" d'utiliser le même objet comme verrou. Par exemple :

void Foo() { lock (a) { ... } }
void Bar() { lock (b) { ... } }

Si les deux méthodes manipulent la même variable, mais que les objets a et b sont différents, vous avez créé une protection factice — les threads travailleront simultanément sur la variable !

Conclusion : utilisez toujours le même objet pour protéger les mêmes données.

Un autre cas — utiliser un verrou trop "large". Par exemple, faire lock (this) dans une classe normale, si vous n'êtes pas sûr que personne n'extérieur n'utilise cet objet pour lock. Ça peut aussi causer des deadlocks et d'autres bugs amusants mais indésirables.

Et enfin : NE bloquez PAS des opérations longues ou externes (travail avec des fichiers, réseau) à l'intérieur d'un lock. Vous risquez de bloquer l'accès aux autres threads pendant longtemps, ce qui réduit la performance. Section critique = uniquement ce qui ne peut vraiment pas être fait en parallèle !

2
Mission
C# SELF, niveau 56, leçon 1
Bloqué
Synchronisation la plus simple avec `lock`
Synchronisation la plus simple avec `lock`
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION