1. Les conditions classiques de Coffman
En 1971, l'informaticien Edward G. Coffman Jr. a décrit quatre conditions sans lesquelles le deadlock est impossible. Ces règles sont devenues l’«ABC» des entretiens et des cours sur le multithreading. Il est utile de les connaître non seulement par cœur, mais aussi de comprendre pourquoi elles existent :
- Mutual Exclusion (Exclusion Mutuelle) : au moins une ressource ne peut être détenue que par un seul thread à la fois.
- Hold and Wait (Maintien et Attente) : un thread ayant acquis une ressource peut attendre d'autres ressources.
- No Preemption (Pas de Préemption) : on ne peut pas retirer une ressource d’un thread de force — seul le thread peut la libérer.
- Circular Wait (Attente Circulaire) : il existe une chaîne de threads où chacun attend une ressource détenue par le suivant.
Si toutes les quatre conditions sont vraies, un deadlock est possible. Si l'une est violée — la probabilité de blocage mutuel disparaît.
Prise de décision rapide
flowchart TD
A[Besoin de locker plusieurs ressources ?] -- Non --> B[lock standard]
A -- Oui --> C[Peut-on toujours prendre dans le même ordre ?]
C -- Oui --> D[Toujours prendre dans le même ordre]
C -- Non --> E[Éviter les lock imbriqués]
D --> F[Minimiser le temps sous le verrou]
E --> F
B --> F
F --> G{Toujours inquiet ?}
G -- Oui --> H[Ajouter des timeouts et réessayer]
G -- Non --> I[Dormez tranquille !]
2. Stratégies principales pour prévenir le Deadlock
Puisque le deadlock fait partie du monde multithread, notre tâche est d’apprendre à coexister avec lui. Différentes stratégies aident : des plus simples aux plus astucieuses.
Toujours locker les ressources dans le même ordre
L'idée est simple : si tous les threads acquièrent toujours les ressources dans le même ordre, l'attente circulaire ne peut tout simplement pas apparaître. Cette technique s'appelle souvent ordered locking — «verrouillage ordonné».
Exemple
Code dangereux :
// Thread 1
lock (resA)
{
lock (resB)
{
// ...
}
}
// Thread 2
lock (resB)
{
lock (resA)
{
// ...
}
}
Ici un deadlock est possible : le premier thread a resA et attend resB, le deuxième est l'inverse. Ils restent bloqués et ne se libèrent jamais.
Code correct :
// Toujours : d'abord resA, puis resB (jamais l'inverse)
lock (resA)
{
lock (resB)
{
// ...
}
}
Maintenant l'ordre est uniforme et le deadlock est impossible. Peu importe combien de ressources vous avez ni comment elles s'appellent — l'important est que tous les threads prennent les locks dans le même ordre.
Erreur typique : si dans le code il y a au moins un endroit où l'ordre est violé, le risque de deadlock revient. Soyez donc vigilants !
Évitez de locker plusieurs ressources en même temps
C'est la plus simple : si vous pouvez éviter d'acquérir des locks sur deux ressources ou plus simultanément — ne le faites pas ! Plus il y a d'locks imbriqués, plus le risque de blocage est élevé. À la place, on peut copier les données nécessaires dans des variables temporaires, libérer le verrou, puis travailler avec d'autres ressources.
Utilisez des timeouts lors de l'acquisition d'un lock
Si vous devez absolument prendre plusieurs locks, les timeouts peuvent sauver la situation. Par exemple, au lieu du lock classique, utilisez des méthodes qui permettent de «essayer» d'acquérir le verrou et, si ça échoue, de tout nettoyer proprement, comme Monitor.TryEnter.
object resourceA = new object();
object resourceB = new object();
bool successA = false, successB = false;
try
{
// On essaie de prendre les deux locks au maximum pendant 2 secondes
successA = Monitor.TryEnter(resourceA, TimeSpan.FromSeconds(2));
if (!successA) return; // pas réussi, on sort
successB = Monitor.TryEnter(resourceB, TimeSpan.FromSeconds(2));
if (!successB) return;
// Section critique
}
finally
{
if (successB) Monitor.Exit(resourceB);
if (successA) Monitor.Exit(resourceA);
}
Si le deuxième lock ne peut pas être pris en 2 secondes, on libère le premier et on sort. Résultat : pas de deadlock, seulement des threads qui abandonnent leur tentative.
Minimisez la quantité de code à l'intérieur du verrou
Plus la section de code sous le verrou est courte, moins de temps une autre branche devra attendre. Évitez d'effectuer des calculs lourds, des appels réseau ou des opérations disque dans la section critique. Acquérir le lock — modifier la ressource partagée rapidement — relâcher. Le reste faites-le en dehors.
Séparez l'état — évitez les ressources partagées
Parfois il vaut mieux éviter l'état partagé plutôt que se battre avec les locks :
- Utilisez des objets immuables (immutable).
- Travaillez sur des copies ou des variables locales au lieu de globals.
- Transmettez les données par messages (Actor Model, queue de messages).
- Utilisez des collections thread-safe : ConcurrentDictionary, ConcurrentQueue etc. de System.Collections.Concurrent.
3. Résolution du Deadlock s'il est déjà survenu
Tuer et redémarrer manuellement (mauvaise option)
La façon la plus simple est de terminer tous les processus bloqués. Mais cela risque de provoquer une perte de données. C'est l'ultime recours à éviter si possible.
Détection du Deadlock en cours d'exécution
Certaines systèmes permettent de détecter automatiquement un deadlock. Si un thread ne peut pas acquérir un verrou trop longtemps, il peut logguer l'événement, alerter le monitoring et initier un rétablissement.
if (!Monitor.TryEnter(resource, TimeSpan.FromSeconds(10)))
{
Console.WriteLine("On dirait qu'on est dans un deadlock ou que quelqu'un tient le verrou trop longtemps !");
// On peut initier la récupération, produire un dump ou notifier l'utilisateur
}
Dans les systèmes distribués, on utilise des détecteurs séparés qui analysent le graphe d'attente et forcent le déblocage des transactions coincées.
Rollback automatique et nouvelle tentative
Bonne pratique : «abandonner» et réessayer — si on ne peut pas acquérir tous les locks dans un délai raisonnable, on rollback, on attend un peu (souvent un délai aléatoire pour éviter la collision) et on retente.
for (int attempt = 0; attempt < 3; attempt++)
{
bool gotRes1 = Monitor.TryEnter(res1, TimeSpan.FromSeconds(2));
bool gotRes2 = false;
try
{
if (gotRes1)
{
gotRes2 = Monitor.TryEnter(res2, TimeSpan.FromSeconds(2));
if (gotRes2)
{
// Section critique
break;
}
}
}
finally
{
if (gotRes2) Monitor.Exit(res2);
if (gotRes1) Monitor.Exit(res1);
}
// Pas réussi — on attend et on réessaie
Thread.Sleep(500 + new Random().Next(500));
}
4. Bonnes pratiques et recommandations
- Analysez l'ordre d'acquisition des locks. Recensez les points d'entrée et vérifiez l'ordre.
- Dans la mesure du possible, utilisez les collections de System.Collections.Concurrent.
- Utilisez des outils d'analyse de code. Des IDE comme Rider ou Visual Studio aident à repérer les locks imbriqués.
- Logguez les tentatives longues d'acquisition.
- Faites des tests de stress des composants multithread.
- Documentez les conventions d'ordre des locks. Les commentaires sauvent de violer l'ordre.
5. Exemples pratiques
Exemple : Base de données
Dans les SGBD le deadlock est fréquent : deux transactions mettent à jour les mêmes tables mais dans des ordres différents (A→B et B→A). La plupart des SGBD peuvent détecter ces situations et «tuer» l'une des transactions.
Exemple : Méthodes asynchrones
En .NET, si on mélange asynchronisme et locks, on peut obtenir un blocage mutuel : await peut suspendre un thread qui tient un lock, tandis qu'un autre thread attend cette même ressource. Planifiez les frontières des sections critiques et évitez les attentes à l'intérieur.
6. Erreurs typiques en travaillant avec les locks
Erreur n°1 : locker des string.
lock (myString) — mauvaise idée. En .NET les chaînes sont internées, donc vous lockez en pratique la table globale des strings.
Erreur n°2 : ordre d'acquisition des objets différent.
Si dans différentes parties du programme les locks sont pris dans des ordres différents, la probabilité d'un deadlock augmente fortement.
Erreur n°3 : verrous trop longs.
Garder un lock plus longtemps que nécessaire, ou appeler des méthodes externes dans la section critique — recette sûre pour des blocages et des freezes.
GO TO FULL VERSION