CodeGym /Cours /C# SELF /Verrouillages mutuels : Deadlocks

Verrouillages mutuels : Deadlocks

C# SELF
Niveau 57, Leçon 1
Disponible

1. Exemple de Deadlock en C#

Deadlock — c'est la situation où chacun dans un groupe de threads prend une ressource et attend la libération d'une autre ressource qui est prise par un autre thread du même groupe — et personne n'obtient rien.

Voyons ça en pratique. Supposons qu'on a deux objets de verrouillage et deux threads. Chaque thread verrouille un objet puis tente d'acquérir le second.

using System;
using System.Threading;

class Program
{
    static readonly object lockerA = new object();
    static readonly object lockerB = new object();

    static void Main()
    {
        Thread thread1 = new Thread(Thread1Work);
        Thread thread2 = new Thread(Thread2Work);

        thread1.Start();
        thread2.Start();

        thread1.Join();
        thread2.Join();

        Console.WriteLine("Les deux threads ont terminé (si aucun Deadlock ne s'est produit)");
    }

    static void Thread1Work()
    {
        lock (lockerA)
        {
            Console.WriteLine("Thread 1 : a acquis lockerA");
            Thread.Sleep(100); // Donner une chance au second thread d'acquérir lockerB

            lock (lockerB)
            {
                Console.WriteLine("Thread 1 : a acquis lockerB");
            }
        }
    }

    static void Thread2Work()
    {
        lock (lockerB)
        {
            Console.WriteLine("Thread 2 : a acquis lockerB");
            Thread.Sleep(100); // Donner une chance au premier thread d'acquérir lockerA

            lock (lockerA)
            {
                Console.WriteLine("Thread 2 : a acquis lockerA");
            }
        }
    }
}

Lancez ce code plusieurs fois — et vous rencontrerez presque certainement que l'application « bloque ». Deadlock arrivé ! Chaque thread a acquis son lock et attend que l'autre libère le second.

Visualisation du Deadlock

Voici un schéma de ce que ça donne :

sequenceDiagram
    participant Thread_1
    participant Thread_2
    participant lockerA
    participant lockerB

    Thread_1->>lockerA: acquérir lockerA
    Thread_2->>lockerB: acquérir lockerB
    Thread_1->>lockerB: attend la libération de lockerB
    Thread_2->>lockerA: attend la libération de lockerA

En résultat : Thread 1 détient lockerA et attend lockerB, Thread 2 détient lockerB et attend lockerA. Personne ne cède — le programme est bloqué.

2. Comment détecter un Deadlock ?

Pourquoi cela arrive ?

Pour comprendre la cause, regardons quelques éléments :

  • Multiples verrouillages : Quand un thread acquiert plusieurs locks en même temps.
  • Ordre de verrouillage incorrect : Si les threads acquièrent les locks dans des ordres différents, un deadlock peut survenir.
  • Possibilité d'attente : Un thread attend la libération d'une ressource qui est détenue par un autre thread.
  • Pas de forçage : Un thread ne peut pas « arracher » le lock à un autre thread en C# — il doit simplement attendre.

Le « piège » classique : si vous avez N threads et N ressources, et que les threads verrouillent dans des ordres différents, vous êtes sur le point d'un deadlock.

Signes typiques d'un Deadlock

Du point de vue extérieur, un deadlock ressemble souvent à un simple « gel » du programme. Les threads sont vivants mais bloqués en attente d'accès aux ressources, se bloquant indéfiniment les uns les autres.

Signes typiques :

  • Le programme a arrêté de répondre de façon inattendue (parfois seulement sous charge).
  • Le debugger montre que les threads sont coincés sur lock, Monitor.Enter, WaitOne, EnterReadLock ou des opérations de synchronisation similaires.
  • Les outils système (par exemple Task Manager ou les outils de Rider/Visual Studio) montrent 0% d'utilisation CPU — « tout est à l'arrêt ».
  • Dans les logs — aucune erreur, mais aucune activité non plus.

Si vous utilisez JetBrains Rider ou Visual Studio, regardez avec le debugger où vos threads sont bloqués (Stack Trace). Si vous voyez plusieurs threads coincés sur lock/Mutex.WaitOne les uns sur les autres — félicitations (ou plutôt, compassion) : c'est un deadlock !

Scénarios classiques de Deadlock

Ordre de verrouillage non coordonné. Comme dans notre exemple : Thread 1 verrouille lockerA, puis lockerB ; Thread 2 fait l'inverse. Voilà, le piège est prêt.

Verrouillages imbriqués. Quand dans un bloc lock vous faites un autre lock sur un autre objet.

Ressources croisées dans des méthodes. C'est particulièrement difficile à suivre si les locks sont dispersés dans différentes méthodes/classes et apparaissent dans des combinaisons variées. Les scénarios se compliquent quand il y a plus de deux locks.

3. Prévention du Deadlock

Bonne nouvelle : on peut se protéger contre les deadlocks !
Mauvaise nouvelle : il faut être attentif lors de la conception du code multithread.

Voici quelques recommandations pratiques (retenez-les — elles vous aideront aussi en entretien) :

Toujours acquérir les locks dans le même ordre

Si vous avez plusieurs objets qui peuvent être verrouillés ensemble — définissez un ordre commun (par exemple par nom ou par index) et respectez-le toujours.

Exemple — acquisition sûre des locks

static void SafeLock(object objA, object objB)
{
    // On compare les références/hashcodes — pour que les threads verrouillent toujours les objets dans le même ordre
    object first = objA.GetHashCode() < objB.GetHashCode() ? objA : objB;
    object second = objA.GetHashCode() < objB.GetHashCode() ? objB : objA;

    lock (first)
    {
        lock (second)
        {
            // section critique
        }
    }
}

Ici — les deux threads entreront d'abord dans lock(objA), puis dans lock(objB), si objA est « plus petit » que objB.

Utilisez des timeouts

Si un thread ne peut pas acquérir un lock en un temps raisonnable — qu'il lance une exception ou sorte proprement. Monitor.TryEnter convient parfaitement.

Exemple avec Monitor.TryEnter

bool lockTakenA = false;
bool lockTakenB = false;

try
{
    Monitor.TryEnter(lockerA, 500, ref lockTakenA);
    if (!lockTakenA)
    {
        // Impossible d'obtenir le lock en 500 ms — on abandonne pour éviter le deadlock
        return;
    }

    Monitor.TryEnter(lockerB, 500, ref lockTakenB);
    if (!lockTakenB)
    {
        return;
    }

    // Section critique...

}
finally
{
    if (lockTakenB)
        Monitor.Exit(lockerB);
    if (lockTakenA)
        Monitor.Exit(lockerA);
}

Si on n'a pas obtenu le lock — on ne reste pas bloqué !

Minimisez la durée des locks

Dans les sections critiques, gardez uniquement le code strictement nécessaire.

Ne mélangez pas différents mécanismes de synchronisation

Si vous utilisez simultanément lock, Mutex, Semaphore, ReaderWriterLockSlim — le risque de vous emmêler et d'obtenir un deadlock augmente.

4. Deadlock avec Mutex, Semaphore et ReaderWriterLockSlim

Les deadlocks peuvent survenir non seulement avec le classique lock ou Monitor, mais aussi avec d'autres mécanismes de synchronisation.

Deadlock avec Mutex

static Mutex mutexA = new Mutex();
static Mutex mutexB = new Mutex();

void Work1()
{
    mutexA.WaitOne();
    Thread.Sleep(100);
    mutexB.WaitOne();

    // Section critique

    mutexB.ReleaseMutex();
    mutexA.ReleaseMutex();
}

Si un deuxième thread les acquiert dans l'ordre inverse — vous aurez un deadlock.

Deadlock avec ReaderWriterLockSlim

ReaderWriterLockSlim, malgré sa flexibilité, ne protège pas contre les deadlocks si un thread a déjà acquis un type de lock et tente d'obtenir un autre.

Par exemple, si un thread détient un read-lock (EnterReadLock) et tente d'entrer en write-lock (EnterWriteLock) — le deadlock est assuré, parce que le write-lock ne peut pas être obtenu tant que tous les read-locks ne sont pas libérés.

5. Comment éviter le Deadlock dans des applications réelles

Voyons comment ça peut ressembler dans notre application de démonstration. Supposons qu'on développe un simulateur de boutique en ligne où travaillent en même temps :

  • Des threads qui mettent à jour les stocks (writers)
  • Des threads qui vérifient la disponibilité des produits (readers)
  • Un administrateur (thread spécial) qui lit et écrit

Mauvais :

lock (stockLock)
{
    // mise à jour du stock
    lock (userLock)
    {
        // quelque chose avec les utilisateurs
    }
}
// ... et ailleurs l'inverse !
lock (userLock)
{
    lock (stockLock) { }
}

Bon :

Mettez-vous d'accord — tentez toujours d'abord de verrouiller stockLock, puis userLock dans tous les threads.

6. Comment ne pas se faire piéger par un Deadlock au travail ou en entretien

Dans les projets réels :

  • Lors de la conception — dessinez le schéma des threads et des verrouillages (papier/tableau).
  • Partagez la convention d'ordre de verrouillage avec toute l'équipe.
  • Utilisez des outils d'analyse statique (Rider/Visual Studio, ReSharper) — ils peuvent détecter des deadlocks potentiels.
  • Lisez les articles sur Microsoft Docs à propos des Deadlocks.

À l'entretien :

  • Montrez que vous comprenez le problème et ses symptômes classiques.
  • Mentionnez les principales stratégies de protection : ordre unique, timeouts (TryEnter), sections critiques minimales.
  • Donnez l'exemple avec TryEnter et expliquez pourquoi il est important de libérer toutes les ressources acquises dans le finally.

7. Erreurs typiques et deadlocks inattendus

Erreur n°1 : des verrouillages non évidents dans des bibliothèques tierces.
Classique — du logging à l'intérieur d'une section critique. Le thread se coince, et vous ne soupçonnez même pas que c'est à cause du code externe.

Erreur n°2 : re-verrouillage du même resource.
C'est le reentrant deadlock : le thread tente d'entrer dans un lock déjà acquis, mais le mécanisme ne supporte pas la réentrance.

Erreur n°3 : utilisation de méthodes asynchrones à l'intérieur des sections critiques.
Particulièrement dangereux avec await : le thread peut « lâcher » le contrôle au pire moment et bloquer tout le système.

2
Mission
C# SELF, niveau 57, leçon 1
Bloqué
Démonstration d'un Deadlock
Démonstration d'un Deadlock
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION