CodeGym /Cours /C# SELF /Race Condition (état de course)

Race Condition (état de course)

C# SELF
Niveau 55, Leçon 4
Disponible

1. Introduction

Dans les applications multithread, la présence d'un race condition — ce n'est pas une question de "si", mais de "quand ça arrivera". Même si tu penses que ton code est fiable, et que tu n'as "que deux petits threads" où "tout est évident et simple", l'état de course peut se cacher dans la partie la plus anodine de la logique.

Qu'est-ce qu'un race condition et pourquoi c'est terrifiant ? Imagine que deux personnes essaient d'éditer simultanément le même papier — l'un écrit, l'autre efface. Parfois tout va bien, parfois le résultat est illisible. En programmation les conséquences peuvent être encore plus "amusantes" : les bugs n'apparaissent pas toujours, seulement dans certaines conditions quasi-aléatoires.

Race condition (état de course) — situation où le résultat de l'exécution du programme dépend de quel thread a accédé en premier à une ressource ou a effectué une action. Ce problème n'apparaît que lors d'accès concurrent (multithread), quand deux threads ou plus touchent des données ou ressources partagées.

Que se passe-t-il lors d'une course ?

Voici un schéma simple. Imagine qu'on a deux threads et une ressource partagée (par ex. la variable X) :


     +---------+           +---------+          
     | Thread 1|           | Thread 2|          
     +----+----+           +----+----+          
          |                     |              
          |     Lecture de X    |              
          | <-------------------|              
          |                     |              
          |     Incrément de X  |              
          |-------------------> |              
          |                     |              
          |     Écriture de X   |              
          | <-------------------|              

Si les deux threads lisent simultanément la variable X, l'augmentent et l'écrivent, l'un peut écraser les changements de l'autre, et le nombre total d'incréments ne correspondra pas à ce qu'on attend.

2. Exemple classique de Race Condition

Voyons ça sur un exemple. Supposons qu'on veuille compter le nombre de clics sur un bouton provenant de différents threads ou le nombre de tâches traitées.

On prend une variable simple et plusieurs threads qui l'incrémentent :

using System;
using System.Threading;

class Program
{
    static int counter = 0; // Ressource partagée

    static void Main()
    {
        Thread t1 = new Thread(IncrementCounter);
        Thread t2 = new Thread(IncrementCounter);

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

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

        Console.WriteLine("Valeur attendue : 200000");
        Console.WriteLine("Valeur réelle : " + counter);
    }

    static void IncrementCounter()
    {
        for (int i = 0; i < 100_000; i++)
        {
            counter++; // << Ici un problème peut survenir !
        }
    }
}

Qu'est-ce qu'on attend ?

Puisque chaque thread incrémente counter de 100000 fois, on s'attend à ce que la valeur finale soit 200000.

Qu'obtient-on en réalité ?

Parfois — oui, 200000. Mais le plus souvent la valeur sera plus petite — parfois beaucoup plus petite. Répète l'expérience, le résultat va varier !

Pourquoi ?

L'opération counter++ n'est pas atomique. En réalité elle se déroule ainsi (simplifié) :

  1. Lire la valeur actuelle de counter (par ex. 0)
  2. Ajouter 1 (on obtient 1)
  3. Écrire la valeur (counter = 1)

Si deux threads lisent l'ancienne valeur en même temps, les deux peuvent écrire la nouvelle valeur, mais en pratique ils appliquent le même incrément.

Visualisation avec deux threads :

Supposons counter = 0.

  • Thread 1 : lit 0
  • Thread 2 : lit 0
  • Thread 1 : calcule 0 + 1 = 1
  • Thread 2 : calcule 0 + 1 = 1
  • Thread 1 : écrit 1
  • Thread 2 : écrit 1 (écrase l'incrément du Thread 1)

"Félicitations", on vient de perdre un incrément ! À l'échelle de milliers ou millions d'opérations — le résultat varie énormément.

3. D'autres exemples : pas seulement l'incrément !

Agitation en cuisine

Pour rendre ça plus parlant, imagine un petit café. Deux cuisiniers font des omelettes sur la même poêle, mais ne coordonnent pas leurs actions :

  • Le premier met une omelette, le second met la sienne dessus — elles se mélangent ;
  • L'un pense "j'ai déjà mis deux omelettes", l'autre pense pareil, mais en réalité il y en a trois alors qu'ils croyaient en avoir quatre ;
  • Le chaos commence...

En programmation le race condition provoque le même "chaos" : le résultat dépend d'une série d'opérations rapides et non contrôlées.

Quand les threads se gênent : accès simultané aux données

Par exemple, tu implémente une application bancaire, et un client crédite et débite simultanément le même compte depuis deux threads (par ex. un virement en ligne et la caisse) :

account.Balance += 500;    // Thread 1 : dépôt
account.Balance -= 300;    // Thread 2 : retrait

Si ces opérations ne sont pas protégées, le solde final peut être incorrect : certaines opérations peuvent tout simplement "être écrasées" si les threads travaillent en même temps.

4. Nuances utiles

Pourquoi le race condition est un problème ?

Difficile à attraper et à reproduire. Le bug peut n'apparaître que sur une machine chargée ou dans des conditions rares.

Difficile à déboguer. En debug, les threads peuvent se comporter différemment et le bug disparaît.

Atteinte de l'intégrité des données. Tu obtiens des données incorrectes, corrompues, parfois sans t'en rendre compte.

Sécurité. Dans des applis critiques, un race condition peut entraîner des fuites, la destruction de données ou même des vulnérabilités.

Diagramme "timing de la course"


+-----------------------+     +-----------------------+
| Thread 1              |     | Thread 2              |
+-----------------------+     +-----------------------+
| 1. Lire counter       |     |                       |
| 2. Incrémenter counter|     |                       |
| (mais pas écrire)     |     |                       |
|                       |     | 1. Lire counter       |
|                       |     | 2. Incrémenter counter|
|                       |     | 3. Écrire counter     |
|                       |     | (counter = 1)         |
| 3. Écrire counter     |     |                       |
| (counter = 1)         |     |                       |
+-----------------------+     +-----------------------+

Les deux threads ont fait l'incrément, mais au final — un seul incrément est enregistré !

Où on rencontre souvent des états de course

  • Toutes variables globales ou statiques accédées par plusieurs threads.
  • Listes, queues, collections qui sont remplies depuis différents threads.
  • Événements et delegates si l'abonnement/désabonnement se fait en même temps (par ex. UI + tâches en arrière-plan).
  • Cache, dictionnaires, gestion des connexions.
  • Toute interaction avec fichiers, logs, bases de données sans transactions ou locks.

Comment éviter le race condition : petit aperçu

  • Synchronisation ! (détails dans les prochaines leçons).
  • Utilise les constructions du langage et des bibliothèques : lock, Monitor, mutexes, sémaphores, etc.
  • Pour les opérations simples — méthodes atomiques (Interlocked.Increment et autres).
  • Utilise des collections thread-safe (ConcurrentBag, ConcurrentDictionary).
  • Pense toujours : "que se passe-t-il si mes deux fonctions sont appelées en même temps ?"

5. Conseils utiles

Conseils pour trouver et diagnostiquer des races

  • Ne fais pas confiance aux opérations les plus simples (incrément ++, affectation), si tu utilises plusieurs threads.
  • Évite autant que possible le partage de variables.
  • Si tu vois des bugs "flottants", des erreurs difficiles à reproduire — pense aux races !
  • Utilise des outils d'analyse des threads (dotTrace, Concurrency Visualizer, Thread Sanitizer).
  • Fais des tests de charge — plus il y a de threads et d'opérations, plus tu as de chances de trouver le bug.

Ce qu'on peut et ne peut pas faire sans synchronisation

Opération Sûr en multithread ? Explication
Affectation int 🟩 Parfois* Seulement si un thread écrit et les autres lisent, sinon — race
Incrément (++/--) 🟥 Non Pas atomique ! Race Condition
Lecture de string 🟩 Parfois* Si la string n'est pas modifiée après création
Affectation d'un objet 🟩 Parfois* À condition qu'il n'y ait pas d'écritures concurrentes
Ajouter dans List<T> 🟥 Non List<T> n'est pas thread-safe
Interlocked.Increment
🟩 Oui Méthode atomique spéciale

— "Parfois" signifie que si un seul thread écrit et tous les autres lisent, c'est sûr ; si plusieurs threads peuvent écrire en même temps — c'est toujours une course.

6. Erreurs typiques et pièges

Dans le code de démonstration ci-dessus on a vu counter++ comme source de problème. Un autre piège : incrémenter ou vérifier une valeur dans une condition.

Exemple : Erreur amusante du "premier lancement"

if (!alreadyStarted)
{
    alreadyStarted = true;
    // On fait l'initialisation...
}

Si plusieurs threads exécutent cette condition en même temps, chacun peut voir alreadyStarted == false et entrer ! Au final — l'initialisation est faite deux fois, ce qui peut provoquer un plantage.

2
Mission
C# SELF, niveau 55, leçon 4
Bloqué
Travailleurs et leurs tâches
Travailleurs et leurs tâches
1
Étude/Quiz
Introduction à la multithreading, niveau 55, leçon 4
Indisponible
Introduction à la multithreading
Bases de la multithreading en C#
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION