CodeGym /Cours /C# SELF /Meilleures pratiques et outils pour le diagnostic

Meilleures pratiques et outils pour le diagnostic

C# SELF
Niveau 57 , Leçon 4
Disponible

1. Le secret d'un code multithread robuste

Si on imagine le multithreading comme une équipe de plusieurs ouvriers réparant une voiture en même temps, c'est clair : si l'un d'eux prend l'outil d'un autre, la réparation s'arrête. Dans le code, c'est pareil : une manipulation imprudente des données partagées mène à des bugs cachés qui n'apparaissent qu'en production.

Dans cette leçon — comment écrire du code multithread qui ne s'effondre pas comme un château de cartes. Et aussi : quels outils utiliser si quelque chose tourne mal.

1. Minimisez les sections critiques (lock)

Moins de code à l'intérieur d'un bloc lock, mieux c'est. Tant qu'un thread tient le verrou, les autres attendent.

Exemple :


// MAUVAIS : Toute la logique métier dans le lock — tous les threads attendent
lock(_locker)
{
    // Opération longue (non liée à la ressource partagée)
    Thread.Sleep(500);
    counter++;
}

// BIEN : Seulement l'action minimale dans le lock
// tout le travail lourd en dehors du lock
Thread.Sleep(500);
lock(_locker)
{
    counter++;
}

Vie réelle : Si un appel réseau ou un calcul long se retrouve dans la région protégée, les performances chutent fortement.

2. N'utilisez pas d'objets génériques comme clé de verrou

Écrire lock(this) ou lock(typeof(MyClass)) — c'est une mauvaise idée.

Pourquoi ? Si quelqu'un d'autre utilise le même objet pour son propre verrou, vous obtiendrez des deadlocks ou des bugs cachés. Utilisez toujours un objet privé dédié :

private readonly object _locker = new object();

lock(_locker)
{
    // Vos actions
}

Interdit : les strings (string), les champs publics, les objets value-type.

3. Toujours utiliser try...finally pour libérer les ressources acquises

Toute acquisition d'un Mutex, d'un sémaphore, de ReaderWriterLockSlim — doit être libérée dans un finally.

_mutex.WaitOne();
try
{
    // Section critique
}
finally
{
    _mutex.ReleaseMutex();
}

4. Ne sur-synchronisez pas

Synchronisez seulement l'accès aux vraies ressources partagées (par ex. collections), pas pour chaque petit truc. Des verrous superflus transforment le code en file d'attente.

5. Utilisez des collections et types thread-safe

.NET fournit des collections spéciales pour les scénarios multithread : ConcurrentDictionary, ConcurrentQueue, ConcurrentBag, BlockingCollection, etc. Elles se protègent déjà en interne.

using System.Collections.Concurrent;

ConcurrentDictionary<int, string> users = new ConcurrentDictionary<int, string>();
users.TryAdd(1, "User1");
users[2] = "User2";

6. Méfiez-vous des deadlocks (verrouillage mutuel)

Piège typique — acquérir plusieurs verrous dans des ordres différents.

// Thread 1
lock(obj1)
{
    lock(obj2)
    {
        // On fait quelque chose
    }
}

// Thread 2
lock(obj2)
{
    lock(obj1)
    {
        // On fait quelque chose
    }
}

Astuce : Toujours acquérir les verrous dans le même ordre dans tous les threads.

7. Quand c'est possible, utilisez l'état immuable (immutable state)

Si un objet ne change pas d'état après sa création — il est sûr à lire depuis n'importe quel thread. Exemples : string, Tuple, DateTime, vos DTO en lecture seule.

2. Outils pour diagnostiquer les problèmes multithread

Les bugs de synchronisation sont sournois : ils apparaissent rarement et de façon imprévisible. Utilisez des outils et des approches qui aident à les détecter et à les analyser.

1. Logging des événements et des threads

Logguez l'Thread.ManagedThreadId actuel et les opérations clés — c'est un moyen simple de comprendre "qui et quand" est entré/sorti d'une section critique.

Console.WriteLine($"[{Thread.CurrentThread.ManagedThreadId}] Est entré dans la section critique");
// ...
Console.WriteLine($"[{Thread.CurrentThread.ManagedThreadId}] Est sorti de la section critique");

Pour des applis réelles, utilisez Microsoft.Extensions.Logging, NLog, Serilog.

2. Thread Sanitizer & Race Detector

Il n'y a pas de ThreadSanitizer "parfait" intégré à .NET, mais il y a des outils utiles :

3. Visual Studio Diagnostics Tools

Les profilers de Visual Studio aident à voir :

  • quels threads existent dans l'application ;
  • où les threads sont en attente (waiting) ;
  • où se produisent des verrous et de la compétition pour les locks ;
  • quand apparaissent deadlocks et contentions.

Prenez des traces pour obtenir un graphe détaillé de l'utilisation des verrous.

4. Analyse de dumps et WinDbg

Si le serveur "freeze", prenez un dump du process et ouvrez-le dans WinDbg ou dotnet-dump. Les stacks montrent où les threads sont bloqués et qui tient quels verrous.

Exemple d'analyse de stacks :

0:000> !syncblk
Index SyncBlock MonitorHeld Recursion Owning Thread Info  SyncBlock Owner
    1 000001d4b6f90e08          1         1 000001d4b5c941c0 000001d4b6f03458

(On fait appel aux dumps surtout pour les "jedi" expérimentés du déploiement — n'ayez pas peur, c'est un outil puissant.)

5. Tests unitaires de charge (stress testing)

Exécutez le code multithread en parallèle des centaines/milliers d'itérations — les races rares sont plus faciles à trouver ainsi.

[Test]
public void Counter_IsThreadSafe()
{
    var counter = 0;
    var locker = new object();
    var tasks = new List<Task>();
    for (int i = 0; i < 100; i++)
    {
        tasks.Add(Task.Run(() =>
        {
            for (int j = 0; j < 10000; j++)
            {
                lock (locker)
                {
                    counter++;
                }
            }
        }));
    }
    Task.WaitAll(tasks.ToArray());
    Assert.AreEqual(100 * 10000, counter);
}

6. Utilisation d'asserts et de vérifications spéciales

Ajoutez des checks qui garantissent un état correct en debug. Par exemple, Debug.Assert lors d'une tentative de réacquisition d'une ressource par le même thread.

3. Conclusions et recommandations

Schéma visuel : zone dangereuse et sécurité

graph TD
    A[Ressource partagée] -- sans synchronisation --> B(Condition de course)
    A -- verrouillage (lock/Mutex) --> C[Accès sûr : section critique]
    C -- "trop de verrous" --> D(Perte de performance)
    A -- ReaderWriterLockSlim --> E{Beaucoup de lecteurs / Un seul écrivain}
    E -- "Lecture" --> F[Beaucoup de threads lisent en même temps]
    E -- "Écriture" --> G[Un seul écrit, les autres attendent]

Primitives de synchronisation et leur usage

Primitif À quoi ça sert Combien de threads laisse passer Inter-processus Performance Où l'utiliser
lock (Monitor)
Section critique simple 1 Non Très élevée 99% des cas
Mutex
Même chose, mais entre processus 1 Oui Moyenne Fichiers, IPC
Semaphore
Pas plus de N threads N Oui Moyenne Pools de ressources
SemaphoreSlim
Idem, mais plus rapide, intra-processus N Non Élevée Pools en code
ReaderWriterLockSlim
Beaucoup de lecteurs, un seul écrivain Beaucoup/1 Non Élevée Caches, settings
2
Mission
C# SELF, niveau 57, leçon 4
Bloqué
Élimination du Livelock avec des pauses aléatoires
Élimination du Livelock avec des pauses aléatoires
1
Étude/Quiz
Deadlocks mutuels, niveau 57, leçon 4
Indisponible
Deadlocks mutuels
Problèmes typiques du multithreading
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION