Synchronisation des Threads

Développement système natif en c/c++ avec win32 ...

Moderator: Rick

Post Reply
Hydraxx
Site Admin
Posts: 46
Joined: Mon Jan 12, 2026 4:04 pm
Location: France
Contact:

Synchronisation des Threads

Post by Hydraxx »

Salut, :D

si tu as suivi le cours précédent, tu sais maintenant créer un thread avec CreateThread. C’est bien, mais ce n’est en réalité que le début. Le vrai problème arrive dès qu’on fait travailler plusieurs threads sur les memes données. A ce moment-là, on ne parle plus seulement d’exécution concurrente, on parle surtout de coordination. Et c’est précisément la que beaucoup de programmes deviennent fragiles.

Pourquoi la synchronisation existe

Tant qu’un thread travaille seul dans son coin, tout va bien. Le code peut rester simple. Mais dans un vrai programme, plusieurs threads finissent très souvent par partager des ressources communes. Cela peut etre une variable globale, une structure allouée sur le heap, un compteur, une file de travail, ou meme un objet plus complexe.

Dans un meme processus, les threads partagent notamment :
  • la mémoire du processus
  • les variables globales et statiques
  • le heap
  • les objets et structures accessibles par pointeur
C’est pratique, parce que cela permet à plusieurs threads de coopérer. Mais c’est aussi ce qui rend le multithreading dangereux. Si deux threads accèdent en meme temps à la meme ressource, et qu’au moins l’un des deux écrit dedans, alors le résultat peut devenir imprévisible.

Parfois le programme plante franchement. Parfois il ne plante pas, ce qui est encore pire, parce qu’il donne l’impression de fonctionner alors qu’il produit un état incohérent. Ce genre de bug est typiquement le plus pénible à retrouver, car il ne se reproduit pas forcément à chaque exécution.

Ce phénomène porte un nom : la race condition.

Autrement dit, la synchronisation ne sert pas à “faire joli” ni à compliquer inutilement le code. Elle sert à éviter qu’un programme multithread devienne instable, non déterministe, ou carrément faux.

Les mécanismes de synchronisation fournis par Win32

Windows propose plusieurs outils de synchronisation. Ils ne sont pas interchangeables, et surtout ils ne répondent pas tous au meme besoin. L’erreur classique du débutant consiste à croire qu’un mutex peut tout faire et qu’il suffit de le mettre partout. En pratique, il faut choisir l’outil adapté au problème.

Nous allons voir ici les mécanismes les plus courants :
  • Critical Section
  • Mutex
  • SRW Lock
  • Semaphore
  • Event
La Critical Section

La Critical Section est l’un des mécanismes les plus utilisés en Win32 lorsqu’on travaille à l’intérieur d’un seul processus. Son but est simple : empêcher plusieurs threads d’entrer en meme temps dans une portion de code qui manipule une ressource partagée.

Elle présente plusieurs caractéristiques importantes :
  • elle est rapide
  • elle est conçue pour de la synchronisation intra-processus
  • elle n’est pas faite pour etre partagée entre plusieurs processus
Le principe est le suivant : un thread entre dans la section critique, exécute la portion protégée, puis la quitte. Tant qu’il est à l’intérieur, les autres threads qui veulent entrer doivent attendre.

Exemple :

Code: Select all

CRITICAL_SECTION cs;

InitializeCriticalSection(&cs);

EnterCriticalSection(&cs);
// section critique
sharedValue++;
LeaveCriticalSection(&cs);

DeleteCriticalSection(&cs);
Ce mécanisme convient très bien lorsqu’on veut protéger des variables ou structures partagées entre plusieurs threads du meme processus, avec un cout relativement faible.

En pratique, on utilise une Critical Section quand :
  • tout se passe dans un seul processus
  • la ressource protégée est locale à ce processus
  • on cherche quelque chose de léger et rapide
Le Mutex

Le mutex remplit une fonction proche de celle d’une Critical Section : garantir qu’un seul thread à la fois puisse accéder à une ressource protégée. Mais il est plus lourd, car il repose sur un véritable objet noyau.

Cette différence a des conséquences :
  • un mutex peut etre partagé entre plusieurs processus
  • il est en général plus couteux qu’une Critical Section
  • il s’appuie sur un objet noyau donc sur une mécanique plus lourde
Exemple :

Code: Select all

HANDLE hMutex = CreateMutex(NULL, FALSE, NULL);

WaitForSingleObject(hMutex, INFINITE);
// section protégée
ReleaseMutex(hMutex);

CloseHandle(hMutex);
Le mutex devient utile quand la ressource protégée peut etre partagée au-delà d’un simple processus. Si plusieurs processus doivent se coordonner, la Critical Section ne suffit plus, alors que le mutex peut répondre au besoin.

En revanche, il ne faut pas l’utiliser machinalement partout, car ce n’est pas l’option la plus légère.

Le SRW Lock

Le SRW Lock, pour Slim Reader/Writer Lock, est un mécanisme plus moderne. Son intérêt principal est d’autoriser plusieurs lecteurs simultanés tant qu’aucun thread n’écrit, tout en garantissant un accès exclusif lorsqu’une écriture doit avoir lieu.

Autrement dit :
  • plusieurs threads peuvent lire en meme temps
  • un seul thread peut écrire
  • pendant l’écriture, les autres doivent attendre
C’est très utile lorsqu’une ressource est consultée très souvent, mais modifiée plus rarement.

Exemple :

Code: Select all

SRWLOCK lock = SRWLOCK_INIT;

// lecture
AcquireSRWLockShared(&lock);
// lire les données
ReleaseSRWLockShared(&lock);

// écriture
AcquireSRWLockExclusive(&lock);
// modifier les données
ReleaseSRWLockExclusive(&lock);
Dans un programme où les lectures sont nombreuses et les écritures peu fréquentes, ce mécanisme peut etre plus intéressant qu’un verrou purement exclusif.

Le Semaphore

Le sémaphore n’a pas exactement le meme rôle qu’un mutex. Il ne sert pas à donner un accès exclusif à un seul thread, mais à limiter le nombre de threads pouvant accéder simultanément à une ressource donnée.

On peut le voir comme un compteur. Tant que le compteur reste supérieur à zéro, un thread peut entrer. Lorsqu’un thread quitte la ressource, il rend un “jeton”, ce qui permet à un autre d’entrer à son tour.

Exemple :

Code: Select all

HANDLE hSem = CreateSemaphore(NULL, 2, 2, NULL);

WaitForSingleObject(hSem, INFINITE);
// accès à la ressource
ReleaseSemaphore(hSem, 1, NULL);

CloseHandle(hSem);
Dans cet exemple, au maximum deux threads peuvent accéder en meme temps à la ressource protégée.

Le sémaphore est utile dans des cas comme :
  • un pool de connexions
  • un nombre limité de ressources identiques
  • une restriction volontaire du parallélisme
L’Event

L’event joue un rôle différent. Il ne protège pas directement une ressource. Il sert à signaler qu’un état ou qu’une étape a été atteint. C’est donc un mécanisme de coordination plus que de protection.

En pratique, un thread peut signaler un event pour indiquer à un autre thread qu’il peut continuer.

Il existe deux grands modes :
  • auto-reset
  • manual-reset
Exemple :

Code: Select all

HANDLE hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);

// thread A
SetEvent(hEvent);

// thread B
WaitForSingleObject(hEvent, INFINITE);

CloseHandle(hEvent);
Ici, un thread attend un signal, et un autre le déclenche. C’est particulièrement utile pour enchaîner des étapes, réveiller un thread en attente, ou coordonner l’arret propre d’un traitement.

Attendre un objet : WaitForSingleObject

WaitForSingleObject est l’une des fonctions les plus centrales de la synchronisation Win32. Elle permet de bloquer un thread jusqu’à ce qu’un objet devienne signalé.

Selon le type d’objet, cela peut vouloir dire des choses différentes. On peut attendre par exemple :
  • un mutex
  • un event
  • un sémaphore
  • un thread
Exemple :

Code: Select all

WaitForSingleObject(hThread, INFINITE);
Dans ce cas, le thread appelant reste bloqué jusqu’à la fin du thread représenté par hThread.

Le point important à comprendre, c’est que cette fonction n’est pas limitée aux seuls verrous. Elle sert de point commun à une grande partie du modèle de synchronisation Win32.

Attendre plusieurs objets : WaitForMultipleObjects

Quand il faut attendre plusieurs objets à la fois, Windows fournit WaitForMultipleObjects. Cette fonction devient utile dès qu’un thread doit réagir à plusieurs signaux possibles, ou quand une boucle de travail doit pouvoir s’interrompre proprement selon plusieurs conditions.

Signature simplifiée :

Code: Select all

WaitForMultipleObjects(
    count,
    handles,
    FALSE,
    INFINITE
);
Exemple :

Code: Select all

HANDLE handles[2] = { hEvent1, hEvent2 };

WaitForMultipleObjects(2, handles, FALSE, INFINITE);
Dans ce cas précis, avec FALSE, l’attente se termine dès qu’un des deux objets devient signalé.

Ce type d’appel est très pratique pour :
  • attendre plusieurs events
  • surveiller un thread de travail et un signal d’arret
  • gérer proprement plusieurs sources de réveil
Les erreurs classiques en synchronisation

La synchronisation ne pardonne pas grand chose. Le code compile, parfois tourne, puis casse uniquement sous charge ou sur une machine différente. C’est pour ça qu’il faut éviter un certain nombre d’erreurs très courantes.

Parmi les fautes classiques, on retrouve :
  • créer trop de threads sans vraie nécessité
  • mettre des mutex partout sans réfléchir au besoin réel
  • oublier de libérer un verrou
  • laisser une section critique durer trop longtemps
  • mélanger plusieurs mécanismes sans stratégie claire
Le résultat de ce genre d’approche est souvent le meme :
  • performances médiocres
  • blocages
  • bugs aléatoires
  • code difficile à raisonner et à maintenir
Le deadlock

Le deadlock est probablement l’un des problèmes les plus classiques du code multithread. Le principe est simple à comprendre, meme si en pratique il peut etre pénible à diagnostiquer.

Imaginons ceci :
  • le thread A possède la ressource 1 et attend la ressource 2
  • le thread B possède la ressource 2 et attend la ressource 1
Aucun des deux ne peut progresser. Chacun attend que l’autre libère ce dont il a besoin. Le programme ne plante pas forcément, mais il n’avance plus. Il est figé.

Pour réduire le risque de deadlock, quelques règles simples aident beaucoup :
  • toujours acquérir plusieurs verrous dans le meme ordre
  • garder les sections protégées aussi courtes que possible
  • éviter de verrouiller sans avoir clairement identifié ce qu’on protège
  • réduire le nombre de dépendances croisées entre threads
Ce ne sont pas des garanties absolues, mais ce sont de très bonnes habitudes d’architecture.

Ce qu’il faut retenir

Dès qu’un programme fait intervenir plusieurs threads sur des données communes, la synchronisation devient obligatoire. Sans elle, le comportement du code cesse d’etre prévisible.

Chaque mécanisme a son rôle :
  • la Critical Section est rapide et adaptée à l’intra-processus
  • le mutex est plus lourd mais peut servir entre processus
  • le SRW Lock convient bien aux scénarios lecture fréquente / écriture rare
  • le sémaphore limite le nombre d’accès simultanés
  • l’event sert à signaler ou coordonner des étapes
  • WaitForSingleObject permet d’attendre un objet
  • WaitForMultipleObjects permet d’en attendre plusieurs
En réalité, la difficulté du multithreading ne vient pas du fait de créer des threads, mais du fait de les faire coopérer correctement. Quand la synchronisation est mal pensée, le programme devient fragile. Quand elle est bien maitrisée, le code devient au contraire beaucoup plus stable, prévisible et robuste.

Dans les prochains cours, on pourra aller plus loin avec des cas concrets de race condition, des exemples de deadlock volontaire, leur détection, et surtout la manière d’écrire un code multithread propre au lieu d’empiler des verrous au hasard.

Who is online

Users browsing this forum: No registered users and 0 guests