🔝 Retour au Sommaire
Imaginez que vous organisez une cuisine professionnelle avec plusieurs chefs qui travaillent simultanément sur différents plats. Tout fonctionne bien jusqu'à ce que deux chefs essaient d'utiliser le même couteau en même temps, ou qu'un chef attende qu'un autre finisse avant de commencer, créant un blocage. Déboguer une application multi-thread, c'est comme résoudre ces conflits dans une cuisine où tout se passe très vite et de manière imprévisible.
Le multi-threading permet à votre application d'exécuter plusieurs tâches en parallèle, ce qui améliore les performances et la réactivité. Mais il introduit aussi une complexité considérable, rendant le débogage beaucoup plus difficile qu'avec du code séquentiel classique.
Pour un débutant, le débogage multi-thread peut sembler intimidant, mais en comprenant les problèmes courants et en utilisant les bons outils, vous pouvez résoudre même les bugs les plus obscurs.
Le multi-threading est une technique qui permet à votre application d'exécuter plusieurs séquences de code (threads) en même temps. Chaque thread est comme un mini-programme qui s'exécute indépendamment des autres.
Programme mono-thread :
- Comme une personne qui fait une tâche après l'autre
- Laver la vaisselle → Puis cuisiner → Puis nettoyer
- Simple, prévisible, mais potentiellement lent
Programme multi-thread :
- Comme plusieurs personnes qui travaillent en parallèle
- Une personne lave la vaisselle PENDANT qu'une autre cuisine
- Plus rapide, mais nécessite coordination
// Code mono-thread (séquentiel)
procedure ExecuterTaches;
begin
TacheA; // S'exécute
TacheB; // Puis s'exécute
TacheC; // Puis s'exécute
end;
// Code multi-thread (parallèle)
procedure ExecuterTachesParalleles;
begin
TTask.Run(procedure begin TacheA; end); // S'exécute
TTask.Run(procedure begin TacheB; end); // S'exécute en même temps
TTask.Run(procedure begin TacheC; end); // S'exécute en même temps
end;Cas d'usage courants :
- Télécharger des fichiers en arrière-plan
- Traiter de grandes quantités de données
- Maintenir l'interface utilisateur réactive pendant des opérations longues
- Serveurs gérant plusieurs clients simultanément
- Calculs parallèles sur plusieurs cœurs CPU
L'exécution multi-thread est non-déterministe : l'ordre dans lequel les threads s'exécutent change à chaque exécution.
Exemple :
// Thread 1
procedure Thread1;
begin
WriteLn('A');
WriteLn('B');
end;
// Thread 2
procedure Thread2;
begin
WriteLn('X');
WriteLn('Y');
end;Exécution 1 : A, X, B, Y
Exécution 2 : X, A, Y, B
Exécution 3 : A, B, X, Y
Chaque exécution peut produire un ordre différent !
Un bug multi-thread peut :
- Apparaître une fois sur 1000 exécutions
- Se produire uniquement sur certaines machines
- Disparaître quand vous déboguez (effet Heisenberg)
- Changer de comportement selon la charge CPU
Conséquence : "Ça marchait hier !" devient votre phrase préférée.
Deux threads modifient la même donnée en même temps, produisant un résultat incorrect.
var
Compteur: Integer = 0;
// Thread 1
procedure Thread1;
var i: Integer;
begin
for i := 1 to 1000 do
Inc(Compteur); // Compteur = Compteur + 1
end;
// Thread 2
procedure Thread2;
var i: Integer;
begin
for i := 1 to 1000 do
Inc(Compteur); // Compteur = Compteur + 1
end;Résultat attendu : 2000
Résultat obtenu : Parfois 1547, parfois 1823, parfois 2000...
Pourquoi ? Inc(Compteur) n'est pas atomique :
- Lire Compteur (ex: 100)
- Ajouter 1 (101)
- Écrire Compteur (101)
Si les deux threads font ça en même temps, on peut perdre des incréments.
Deux threads s'attendent mutuellement, créant un blocage permanent.
var
Lock1, Lock2: TCriticalSection;
// Thread 1
Lock1.Enter;
// ... travail ...
Lock2.Enter; // Attend Lock2
// ... travail ...
Lock2.Leave;
Lock1.Leave;
// Thread 2
Lock2.Enter;
// ... travail ...
Lock1.Enter; // Attend Lock1
// ... travail ...
Lock1.Leave;
Lock2.Leave;Scénario de deadlock :
- Thread 1 acquiert Lock1
- Thread 2 acquiert Lock2
- Thread 1 attend Lock2 (détenu par Thread 2)
- Thread 2 attend Lock1 (détenu par Thread 1)
- Blocage permanent : Les deux attendent indéfiniment
Un thread accède à un objet pendant qu'un autre le libère :
// Thread 1 : Crée et utilise l'objet
MonObjet := TMonObjet.Create;
MonObjet.Traiter;
// Thread 2 : Libère l'objet
MonObjet.Free;
// Thread 1 : CRASH !
MonObjet.Afficher; // MonObjet est déjà libéréSymptômes :
- Résultats incorrects ou incohérents
- Données corrompues
- Compteurs incorrects
Exemple détaillé :
type
TCompteurNonSecurise = class
private
FValeur: Integer;
public
procedure Incrementer;
function ObtenirValeur: Integer;
end;
procedure TCompteurNonSecurise.Incrementer;
begin
FValeur := FValeur + 1; // ❌ NON thread-safe
end;
function TCompteurNonSecurise.ObtenirValeur: Integer;
begin
Result := FValeur;
end;Solution : Synchronisation
type
TCompteurSecurise = class
private
FValeur: Integer;
FLock: TCriticalSection;
public
constructor Create;
destructor Destroy; override;
procedure Incrementer;
function ObtenirValeur: Integer;
end;
constructor TCompteurSecurise.Create;
begin
FLock := TCriticalSection.Create;
end;
destructor TCompteurSecurise.Destroy;
begin
FLock.Free;
inherited;
end;
procedure TCompteurSecurise.Incrementer;
begin
FLock.Enter;
try
FValeur := FValeur + 1; // ✓ Thread-safe
finally
FLock.Leave;
end;
end;
function TCompteurSecurise.ObtenirValeur: Integer;
begin
FLock.Enter;
try
Result := FValeur;
finally
FLock.Leave;
end;
end;Symptômes :
- L'application se fige complètement
- Aucun message d'erreur
- CPU utilisation à 0%
- Impossible de fermer l'application normalement
Causes fréquentes :
- Acquisition de locks dans un ordre différent
- Attente circulaire entre threads
- Oubli de libérer un lock
Prévention :
// ✓ BON : Toujours acquérir les locks dans le même ordre
procedure SafeOperation;
begin
Lock1.Enter; // Toujours Lock1 d'abord
try
Lock2.Enter; // Puis Lock2
try
// Travail...
finally
Lock2.Leave;
end;
finally
Lock1.Leave;
end;
end;Règle d'or en Delphi : On ne peut JAMAIS accéder aux composants VCL (visuels) directement depuis un thread secondaire.
Code incorrect :
procedure MonThread.Execute;
begin
// ❌ DANGEREUX : Accès VCL depuis un thread
Label1.Caption := 'Traitement...';
Memo1.Lines.Add('Log');
end;Conséquence : Crashes aléatoires, corruption de l'interface, comportement imprévisible.
Code correct :
procedure MonThread.Execute;
begin
// ✓ BON : Utiliser Synchronize
TThread.Synchronize(nil, procedure
begin
Label1.Caption := 'Traitement...';
Memo1.Lines.Add('Log');
end);
end;Synchronize exécute le code dans le thread principal (où la VCL peut être utilisée).
Les objets créés dans un thread doivent être libérés, mais la gestion devient complexe :
procedure MonThread.Execute;
var
Liste: TStringList;
begin
Liste := TStringList.Create;
try
// Travail avec Liste...
// ❌ Problème : Comment passer Liste au thread principal ?
TThread.Synchronize(nil, procedure
begin
AfficherListe(Liste); // Liste existe encore ?
end);
finally
Liste.Free; // Libérée avant que Synchronize ne s'exécute ?
end;
end;Solution correcte :
procedure MonThread.Execute;
var
Liste: TStringList;
Copie: TStringList;
begin
Liste := TStringList.Create;
try
// Travail avec Liste...
// Créer une copie pour le thread principal
Copie := TStringList.Create;
Copie.Assign(Liste);
TThread.Queue(nil, procedure
begin
try
AfficherListe(Copie);
finally
Copie.Free; // Libérée dans le thread principal
end;
end);
finally
Liste.Free;
end;
end;var
Resultat: Integer; // Variable globale
// Thread 1
procedure Thread1;
begin
Resultat := CalculComplexe1; // Écriture
end;
// Thread 2
procedure Thread2;
begin
Resultat := CalculComplexe2; // Écriture
end;
// Thread principal
procedure AfficherResultat;
begin
ShowMessage(IntToStr(Resultat)); // Lecture - quelle valeur ?
end;Problème : Les deux threads écrivent dans Resultat. Le thread principal ne sait pas quand lire une valeur stable.
La fenêtre Threads montre tous les threads actifs dans votre application.
Comment l'ouvrir :
- Démarrez le débogage (F9)
- Mettez en pause l'exécution (point d'arrêt ou pause manuelle)
- Menu View > Debug Windows > Threads
Ce que vous verrez :
ID Status Location
-------------------------------------------
5432 Running TMyThread.Execute
5433 Waiting TWorkerThread.Execute
5434 Suspended System.SysUtils
7890 Running FormMain.ButtonClick (← Thread principal)
Informations affichées :
- ID : Identifiant unique du thread
- Status : État actuel (Running, Waiting, Suspended)
- Location : Où se trouve le thread dans le code
Actions possibles :
- Double-cliquer sur un thread pour voir son code
- Clic droit pour geler/dégeler un thread
- Voir la pile d'appels de chaque thread
Chaque thread a sa propre pile d'appels.
Voir la pile d'un thread spécifique :
- Sélectionnez le thread dans la fenêtre Threads
- La fenêtre Call Stack affiche la pile de CE thread
- Vous voyez la séquence d'appels qui a mené à l'état actuel
Exemple de Call Stack :
Thread 5432:
TMyThread.TraiterDonnees (ligne 145)
TMyThread.Execute (ligne 98)
ThreadProc (System.Classes)
BeginThread (System)
Vous pouvez configurer un point d'arrêt pour ne se déclencher que sur un thread spécifique.
Configuration :
- Placez un point d'arrêt (F5 ou clic dans la marge)
- Clic droit sur le point d'arrêt > Breakpoint Properties
- Dans la fenêtre, section Thread
- Cochez Break when hit in thread et sélectionnez le thread
Usage : Utile quand vous avez 10 threads et que seul le thread #3 pose problème.
Pendant le débogage, vous pouvez "geler" certains threads pour simplifier l'analyse.
Comment :
- Dans la fenêtre Threads
- Clic droit sur un thread
- Choisir Freeze (geler) ou Thaw (dégeler)
Thread gelé : Ne s'exécutera plus jusqu'à ce que vous le dégeliez.
Cas d'usage :
- Vous avez 5 threads worker et voulez tester seulement le thread #2
- Gelez les threads 1, 3, 4, 5
- Déboguez tranquillement le thread #2
Les variables locales de chaque thread sont indépendantes.
Voir les variables d'un thread :
- Sélectionnez le thread dans la fenêtre Threads
- La fenêtre Local Variables montre SES variables locales
- Les variables globales sont partagées entre tous les threads
Exemple :
procedure TMyThread.Execute;
var
CompteurLocal: Integer; // Variable LOCALE à ce thread
begin
CompteurLocal := 0;
TThread.Synchronize(nil, procedure
begin
Inc(CompteurGlobal); // Variable GLOBALE partagée
end);
end;- Chaque thread a son propre
CompteurLocal - Tous partagent le même
CompteurGlobal
Dans le Evaluate/Modify (Ctrl+F7), les expressions sont évaluées dans le contexte du thread actuellement sélectionné.
(Thread 5432 sélectionné)
Evaluate: MonObjet.FValeur
Result: 42
(Thread 5433 sélectionné)
Evaluate: MonObjet.FValeur
Result: 42 (même objet si partagé)
ou différent si objet local
Le logging est votre meilleur ami en multi-threading.
Logger thread-safe simple :
type
TThreadSafeLogger = class
private
FLock: TCriticalSection;
FLogFile: TextFile;
public
constructor Create(const FileName: string);
destructor Destroy; override;
procedure Log(const Message: string);
end;
constructor TThreadSafeLogger.Create(const FileName: string);
begin
FLock := TCriticalSection.Create;
AssignFile(FLogFile, FileName);
Rewrite(FLogFile);
end;
destructor TThreadSafeLogger.Destroy;
begin
CloseFile(FLogFile);
FLock.Free;
inherited;
end;
procedure TThreadSafeLogger.Log(const Message: string);
var
ThreadID: TThreadID;
begin
ThreadID := TThread.CurrentThread.ThreadID;
FLock.Enter;
try
WriteLn(FLogFile, Format('[%s] Thread %d: %s',
[FormatDateTime('hh:nn:ss.zzz', Now),
ThreadID,
Message]));
Flush(FLogFile);
finally
FLock.Leave;
end;
end;Usage :
var
Logger: TThreadSafeLogger;
procedure TMyThread.Execute;
begin
Logger.Log('Thread démarré');
TraiterDonnees;
Logger.Log('Traitement terminé');
Logger.Log('Thread terminé');
end;Fichier log résultant :
[14:32:15.234] Thread 5432: Thread démarré
[14:32:15.237] Thread 5433: Thread démarré
[14:32:15.891] Thread 5432: Traitement terminé
[14:32:16.103] Thread 5433: Traitement terminé
[14:32:16.104] Thread 5432: Thread terminé
[14:32:16.106] Thread 5433: Thread terminé
Vous voyez clairement l'ordre d'exécution et les entrelacements.
Utilisez des assertions pour vérifier vos hypothèses :
procedure TMyThread.Execute;
begin
Assert(TThread.CurrentThread.ThreadID <> MainThreadID,
'Cette méthode ne doit PAS être appelée depuis le thread principal');
// Travail...
FLock.Enter;
try
Assert(FCompteur >= 0, 'Le compteur ne peut pas être négatif');
Dec(FCompteur);
finally
FLock.Leave;
end;
end;Ajoutez des Sleep() pour ralentir l'exécution et rendre les race conditions plus visibles :
procedure TMyThread.Execute;
begin
FLock.Enter;
try
Valeur := Valeur + 1;
Sleep(10); // ← Artificiel, pour tester
Traiter(Valeur);
finally
FLock.Leave;
end;
end;Si un bug apparaît avec Sleep(10), c'est qu'il existe sans Sleep aussi, mais plus rare.
Créez de nombreux threads pour augmenter la probabilité de race conditions :
procedure TesterAvecStress;
var
i: Integer;
Threads: array[0..99] of TThread;
begin
// Créer 100 threads
for i := 0 to 99 do
begin
Threads[i] := TThread.CreateAnonymousThread(procedure
begin
// Code à tester
MonOperationCritique;
end);
Threads[i].Start;
end;
// Attendre que tous terminent
for i := 0 to 99 do
Threads[i].WaitFor;
end;Plus vous avez de threads, plus les problèmes de concurrence apparaîtront rapidement.
Utilisez des compteurs pour tracer l'exécution :
var
AppelsEntree: Integer = 0;
AppelsSortie: Integer = 0;
procedure SectionCritique;
begin
AtomicIncrement(AppelsEntree);
try
// Code critique
finally
AtomicIncrement(AppelsSortie);
end;
end;
// À la fin, vérifier
Assert(AppelsEntree = AppelsSortie,
'Nombre d''entrées doit égaler nombre de sorties');Bien qu'il n'existe pas de Thread Sanitizer natif pour Delphi comme pour C++, vous pouvez implémenter des vérifications similaires :
type
TAccessTracker = class
private
FLastAccessThreadID: TThreadID;
FAccessCount: Integer;
public
procedure RecordAccess;
function DetectRace: Boolean;
end;
procedure TAccessTracker.RecordAccess;
var
CurrentThread: TThreadID;
begin
CurrentThread := TThread.CurrentThread.ThreadID;
if (FLastAccessThreadID <> 0) and
(FLastAccessThreadID <> CurrentThread) and
(FAccessCount > 0) then
begin
// Accès concurrent détecté !
raise Exception.Create('Race condition détectée !');
end;
FLastAccessThreadID := CurrentThread;
Inc(FAccessCount);
end;- Application complètement figée
- Pas de réponse aux clics
- CPU proche de 0%
- Pas de message d'erreur
Étapes :
-
Mettre en pause le programme figé (bouton Pause ou Break)
-
Ouvrir la fenêtre Threads (View > Debug Windows > Threads)
-
Examiner chaque thread :
- Quels sont en état "Waiting" ?
- Où attendent-ils ?
-
Voir les piles d'appels :
- Double-cliquer sur chaque thread waiting
- Regarder la Call Stack
Exemple de ce que vous pourriez voir :
Thread 5432:
TCriticalSection.Enter (System.SyncObjs)
TManager.ProcessA (ligne 145) ← Attend Lock2
Thread 5433:
TCriticalSection.Enter (System.SyncObjs)
TManager.ProcessB (ligne 198) ← Attend Lock1
Analyse : Thread 5432 attend Lock2, Thread 5433 attend Lock1 → Deadlock !
Règle 1 : Ordre d'acquisition cohérent
// ✓ BON : Toujours le même ordre partout
procedure Operation1;
begin
LockA.Enter;
try
LockB.Enter;
try
// ...
finally
LockB.Leave;
end;
finally
LockA.Leave;
end;
end;
procedure Operation2;
begin
LockA.Enter; // Même ordre : A puis B
try
LockB.Enter;
try
// ...
finally
LockB.Leave;
end;
finally
LockA.Leave;
end;
end;Règle 2 : Timeouts
if Lock.TryEnter(1000) then // Attendre max 1 seconde
begin
try
// Travail...
finally
Lock.Leave;
end;
end
else
raise Exception.Create('Timeout : deadlock possible');Règle 3 : Limiter la portée des locks
// ❌ MAUVAIS : Lock tenu longtemps
Lock.Enter;
try
TraitementLong; // 10 secondes
AccesRapide;
finally
Lock.Leave;
end;
// ✓ BON : Lock minimal
TraitementLong; // Sans lock
Lock.Enter;
try
AccesRapide; // Seulement ici
finally
Lock.Leave;
end;- Résultats incohérents
- Données corrompues
- Comportement aléatoire
- Difficile à reproduire
type
TCompteThreadSafe = class
private
FSolde: Currency;
FLock: TCriticalSection;
public
procedure Crediter(Montant: Currency);
procedure Debiter(Montant: Currency);
function ObtenirSolde: Currency;
end;
procedure TCompteThreadSafe.Debiter(Montant: Currency);
var
SoldeAvant, SoldeApres: Currency;
begin
FLock.Enter;
try
SoldeAvant := FSolde;
FSolde := FSolde - Montant;
SoldeApres := FSolde;
// Assertion pour détecter les incohérences
Assert(SoldeApres = SoldeAvant - Montant,
'Race condition : solde incohérent !');
finally
FLock.Leave;
end;
end;var
TotalCredits: Integer = 0;
TotalDebits: Integer = 0;
procedure Crediter(Montant: Currency);
begin
AtomicIncrement(TotalCredits);
// ... opération ...
end;
procedure Debiter(Montant: Currency);
begin
AtomicIncrement(TotalDebits);
// ... opération ...
end;
// Vérification finale
procedure VerifierCoherence;
begin
Assert(TotalCredits + TotalDebits = NombreOperationsAttendu,
'Certaines opérations ont été perdues (race condition)');
end;Ajoutez du code qui détecte les accès concurrents :
type
TProtectedData = class
private
FData: Integer;
FCurrentOwner: TThreadID;
public
procedure BeginAccess;
procedure EndAccess;
procedure SetValue(Value: Integer);
end;
procedure TProtectedData.BeginAccess;
begin
if FCurrentOwner <> 0 then
raise Exception.Create('Accès concurrent détecté !');
FCurrentOwner := TThread.CurrentThread.ThreadID;
end;
procedure TProtectedData.EndAccess;
begin
FCurrentOwner := 0;
end;
procedure TProtectedData.SetValue(Value: Integer);
begin
BeginAccess;
try
FData := Value;
finally
EndAccess;
end;
end;Pour un logging rapide pendant le débogage :
procedure LogThread(const Message: string);
begin
OutputDebugString(PChar(Format('[Thread %d] %s',
[TThread.CurrentThread.ThreadID,
Message])));
end;
// Usage
procedure TMyThread.Execute;
begin
LogThread('Début du traitement');
// ...
LogThread('Fin du traitement');
end;Visible dans la fenêtre Event Log de Delphi.
Si l'application plante en production :
- Configurer Windows pour créer des crash dumps
- Analyser le dump avec WinDbg ou similaire
- Voir l'état de tous les threads au moment du crash
!threads # Liste tous les threads
~*k # Stack trace de tous les threads
Utilisez des outils de profilage (AQtime, Sampling Profiler) pour :
- Identifier les contentions de locks
- Voir quels threads attendent le plus
- Détecter les déséquilibres de charge
Pour les opérations simples, utilisez les fonctions atomiques :
var
Compteur: Integer;
// ✓ Thread-safe sans lock
procedure Incrementer;
begin
AtomicIncrement(Compteur);
end;
// ✓ Thread-safe
function CompareAndSwap(var Target: Integer;
NewValue, Comparand: Integer): Integer;
begin
Result := TInterlocked.CompareExchange(Target, NewValue, Comparand);
end;Avantages :
- Plus rapide que les locks
- Pas de risque de deadlock
Limitations :
- Uniquement pour opérations simples
- Entiers et pointeurs seulement
Pour une initialisation thread-safe paresseuse :
var
Instance: TMonSingleton = nil;
Lock: TCriticalSection;
function ObtenirInstance: TMonSingleton;
begin
if Instance = nil then // Premier check (rapide, sans lock)
begin
Lock.Enter;
try
if Instance = nil then // Second check (avec lock)
Instance := TMonSingleton.Create;
finally
Lock.Leave;
end;
end;
Result := Instance;
end;Pour communiquer entre threads de manière sûre :
type
TQueueThreadSafe<T> = class
private
FQueue: TQueue<T>;
FLock: TCriticalSection;
FEvent: TEvent;
public
constructor Create;
destructor Destroy; override;
procedure Enqueue(Item: T);
function Dequeue(out Item: T; Timeout: Cardinal): Boolean;
end;
// Producer thread
procedure Producer;
var
Item: TWorkItem;
begin
Item := CreerItem;
Queue.Enqueue(Item);
end;
// Consumer thread
procedure Consumer;
var
Item: TWorkItem;
begin
if Queue.Dequeue(Item, 1000) then
begin
TraiterItem(Item);
end;
end;Plutôt que créer/détruire des threads constamment :
type
TThreadPool = class
private
FThreads: TList<TThread>;
FTasks: TThreadedQueue<TProc>;
public
constructor Create(NumThreads: Integer);
destructor Destroy; override;
procedure QueueTask(Task: TProc);
end;
// Usage
ThreadPool.QueueTask(procedure
begin
TraiterDonnees;
end);Delphi fournit TThreadPool dans System.Threading.
Les données immuables (non modifiables) sont naturellement thread-safe :
type
TConfigImmutable = class
private
FServeur: string;
FPort: Integer;
public
constructor Create(const Serveur: string; Port: Integer);
property Serveur: string read FServeur;
property Port: Integer read FPort;
// Pas de setters - immuable !
end;Plusieurs threads peuvent lire sans locks.
Utilisez cette checklist pour diagnostiquer les problèmes :
□ Identification du problème
- Le bug est-il intermittent ou systématique ?
- Apparaît-il uniquement avec plusieurs threads actifs ?
- L'application se fige-t-elle complètement ou partiellement ?
- Y a-t-il un message d'erreur ou un crash silencieux ?
□ Inspection des threads
- Combien de threads sont actifs ?
- Quel est l'état de chaque thread (Running, Waiting) ?
- Où se trouve chaque thread dans le code ?
- Y a-t-il des threads en attente infinie ?
□ Variables partagées
- Quelles variables sont partagées entre threads ?
- Ces variables sont-elles protégées par des locks ?
- Les locks sont-ils toujours relâchés (finally) ?
- Y a-t-il des accès VCL depuis des threads ?
□ Synchronisation
- Les locks sont-ils acquis dans le même ordre partout ?
- Y a-t-il des deadlocks possibles ?
- TThread.Synchronize est-il utilisé pour accéder à la VCL ?
- Les objets partagés sont-ils thread-safe ?
□ Logging et instrumentation
- Y a-t-il du logging thread-safe ?
- Les logs montrent-ils l'ordre d'exécution ?
- Les assertions sont-elles en place ?
- Les compteurs de vérification sont-ils corrects ?
□ Tests
- Le problème est-il reproductible avec du stress testing ?
- L'ajout de Sleep() révèle-t-il le bug ?
- Les tests avec moins/plus de threads changent-ils le comportement ?
Ne vous lancez pas immédiatement dans des architectures multi-thread complexes. Commencez par :
- Un seul thread worker
- Des tâches simples
- Synchronisation basique
TTask de Delphi gère beaucoup de complexité pour vous :
// Simple et sûr
TTask.Run(procedure
begin
// Votre code ici
TraiterDonnees;
// Mise à jour UI
TThread.Synchronize(nil, procedure
begin
Label1.Caption := 'Terminé';
end);
end);Utilisez les classes thread-safe existantes :
TThreadList<T>: Liste thread-safeTThreadedQueue<T>: Queue thread-safeTMonitor: Alternative aux critical sections
Ne découvrez pas les bugs multi-thread en production. Testez dès le début avec :
- Plusieurs threads
- Stress testing
- Sleep() artificiel
/// <summary>
/// Thread worker qui traite les commandes en arrière-plan.
/// Thread-safe : Oui
/// Accès VCL : Via Synchronize uniquement
/// Durée de vie : Créé au démarrage, détruit à la fermeture
/// </summary>
type
TCommandeWorkerThread = class(TThread)Lors du débogage, gelez tous les threads sauf celui qui vous intéresse. Déboguez-le comme du code normal, puis passez au suivant.
Les variables globales partagées sont la source n°1 de problèmes multi-thread.
// ❌ MAUVAIS
var
CompteurGlobal: Integer;
// ✓ BON
type
TManager = class
private
FCompteur: Integer;
FLock: TCriticalSection;
public
function IncrementCompteur: Integer;
end;Le code multi-thread est plus difficile à comprendre et déboguer. N'utilisez le multi-threading que quand :
- Vous en avez vraiment besoin (performance, réactivité)
- Vous comprenez les implications
- Vous pouvez le tester correctement
Les exceptions multi-thread donnent souvent des indices :
- "Access violation" → Accès à objet libéré
- "Cannot call VCL from thread" → Accès VCL incorrect
- Pas d'erreur mais freeze → Deadlock probable
Le débogage multi-thread est difficile même pour les experts. N'hésitez pas à :
- Montrer votre code à un collègue
- Poster sur les forums (avec du code minimal reproductible)
- Utiliser le rubber duck debugging (expliquer à haute voix)
Si vous avez Visual Studio, utilisez son visualiseur de concurrence pour voir :
- Chronologie d'exécution des threads
- Contentions de locks
- Utilisation CPU par thread
Pour l'analyse post-mortem :
- Analyse de crash dumps
- Inspection détaillée des threads
- Commandes puissantes pour le débogage multi-thread
Pour voir les interactions système en temps réel :
- Opérations fichiers
- Opérations réseau
- Par thread
Votre application se fige aléatoirement après quelques minutes d'utilisation.
1. Reproduire le problème
- Exécuter l'application en débogage
- Attendre qu'elle se fige
- Ne pas la fermer !
2. Mettre en pause
- Cliquer sur Pause dans Delphi
- Ou Ctrl+Alt+Break
3. Examiner les threads
View > Debug Windows > Threads
Thread ID Status Location
------------------------------------
1234 Waiting TCriticalSection.Enter
5678 Waiting TCriticalSection.Enter
9012 Running Application.ProcessMessages
Threads 1234 et 5678 sont en "Waiting" → suspect !
4. Call Stack du thread 1234
TCriticalSection.Enter
TOrderManager.ProcessOrder ligne 145
→ Attend FLockB
5. Call Stack du thread 5678
TCriticalSection.Enter
TOrderManager.UpdateInventory ligne 287
→ Attend FLockA
6. Analyse
Thread 1234 attend LockB et détient LockA
Thread 5678 attend LockA et détient LockB
→ Deadlock classique !
7. Solution
Modifier le code pour acquérir les locks dans le même ordre :
// Avant (deadlock possible)
procedure ProcessOrder;
begin
FLockA.Enter;
try
FLockB.Enter; // Ordre différent selon la méthode
try
// ...
finally
FLockB.Leave;
end;
finally
FLockA.Leave;
end;
end;
// Après (cohérent)
procedure ProcessOrder;
begin
FLockA.Enter; // Toujours A puis B
try
FLockB.Enter;
try
// ...
finally
FLockB.Leave;
end;
finally
FLockA.Leave;
end;
end;
procedure UpdateInventory;
begin
FLockA.Enter; // Même ordre : A puis B
try
FLockB.Enter;
try
// ...
finally
FLockB.Leave;
end;
finally
FLockA.Leave;
end;
end;- DocWiki Embarcadero : Threading et synchronisation
- Exemples de code dans les samples Delphi
- "Parallel Programming with Delphi" par Marco Cantù
- "Delphi High Performance" par Primož Gabrijelčič
- DelphiPraxis.net : Section Threading
- Stack Overflow : Tag [delphi] + [multithreading]
- Embarcadero Forums : Delphi RTL section
- Primož Gabrijelčič : Expert en multi-threading Delphi
- Marco Cantù : Auteur et expert Delphi
Le débogage de code multi-thread est l'un des défis les plus complexes en programmation, mais avec les bons outils, techniques et patience, même les bugs les plus obscurs peuvent être résolus.
Points clés à retenir :
Non-déterminisme : L'exécution multi-thread est imprévisible. Acceptez-le et utilisez des techniques qui gèrent cette imprévisibilité.
Outils Delphi : Maîtrisez la fenêtre Threads, Call Stack, et les points d'arrêt spécifiques aux threads.
Logging : Un bon système de logging thread-safe est essentiel. Loggez généreusement.
Synchronisation : Toujours protéger les données partagées avec des locks ou utiliser des opérations atomiques.
VCL et threads : Utilisez TOUJOURS Synchronize/Queue pour accéder à la VCL depuis un thread.
Deadlocks : Acquérez les locks dans un ordre cohérent et utilisez des timeouts.
Race conditions : Utilisez des assertions, des compteurs de vérification et du stress testing.
Simplicité : Le meilleur code multi-thread est celui que vous n'avez pas à écrire. Utilisez TTask et les classes thread-safe de Delphi.
Patience : Les bugs multi-thread sont difficiles à trouver et à corriger. Prenez votre temps, utilisez une approche méthodique.
Tests : Testez avec de nombreux threads, du stress, et des conditions variées. Les bugs multi-thread se cachent bien.
En développant votre expertise en débogage multi-thread, vous devenez capable de créer des applications Delphi performantes et fiables qui exploitent pleinement les capacités des processeurs modernes multi-cœurs. C'est une compétence avancée qui vous distingue en tant que développeur professionnel, capable de résoudre les problèmes les plus difficiles et de livrer des logiciels de haute qualité même dans des environnements concurrents complexes.