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 :
- JetBrains ReSharper — les inspections attrapent certains patterns dangereux.
- Roslyn Analyzers — analyse statique du code.
- Concurrency Visualizer — analyse des attentes/verrouillages et de la charge des threads.
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 |
|---|---|---|---|---|---|
|
Section critique simple | 1 | Non | Très élevée | 99% des cas |
|
Même chose, mais entre processus | 1 | Oui | Moyenne | Fichiers, IPC |
|
Pas plus de N threads | N | Oui | Moyenne | Pools de ressources |
|
Idem, mais plus rapide, intra-processus | N | Non | Élevée | Pools en code |
|
Beaucoup de lecteurs, un seul écrivain | Beaucoup/1 | Non | Élevée | Caches, settings |
GO TO FULL VERSION