Net-Base Magazine

24.07.2026

Delphi: TParallel.For avec interface de progression sûre pour les threads (TThread.Queue) sans interblocages

Comment combiner Delphi TParallel.For avec une interface de progression sécurisée pour les threads : mises à jour via TThread.Queue, agrégation propre, gestion des annulations et pièges d'interblocage typiques lors du débogage et en exploitation.

24.07.2026

Du thème du magazine à la pratique des projets

Pages de services et techniques pertinentes pour l'article

Qui, dans Delphi, souhaite paralléliser des tâches intensives en calcul ou en E/S tombera rapidement sur la Parallel Programming Library (PPL) et plus précisément sur TParallel.For. L’effet est souvent immédiatement mesurable — jusqu’au moment où l’on veut « vite » mettre à jour une Progress-UI. C’est précisément là que surviennent les blocages typiques : des freezes d’interface apparemment aléatoires, une ProgressBar qui recule, ou un deadlock complet dès qu’on avance pas à pas dans le débogueur.

Ce billet porte sur une Progress-UI threadsafe avec TParallel.For : un modèle robuste qui fonctionne en VCL et FMX, qui centralise proprement les mises à jour UI via TThread.Queue, prend en compte Cancel/Abort et évite de manière systématique les pièges de deadlock les plus courants. L’accent est mis sur la réalité opérationnelle : comportement reproductible, responsabilités claires et conseils de débogage utiles même lorsque l’erreur n’apparaît « que chez le client ».

Pourquoi la Progress-UI pose si souvent problème avec TParallel.For

TParallel.For s’exécute typiquement sur des worker-threads issus du pool de threads Delphi. Ces threads ne doivent pas toucher directement des contrôles VCL ou FMX, car les frameworks UI (boucle de messages, handles de fenêtres, rendu) sont liés au Main Thread. Même un ProgressBar.Position := … apparemment inoffensif depuis un worker peut provoquer un comportement indéfini : AV sporadiques, fenêtres figées ou mises à jour « clignotantes ».

La réparation évidente est souvent TThread.Synchronize. Cela résout bien la sécurité des threads, mais dans des boucles parallèles ouvre rapidement un autre problème : un goulot d’étranglement sériel. Chaque worker attend le Main Thread, lui-même occupé au rendu et au traitement des appels Synchronize. Sous charge, cela ressemble à un deadlock — même s’il s’agit « simplement » d’un effet de starvation/lockstep.

Et il existe la classe des deadlocks véritables : le Main Thread attend (par ex. via WaitFor, Task.Wait ou indirectement par des appels bloquants) la fin de l’opération parallèle, tandis que des worker-threads tentent d’envoyer du travail au Main Thread via Synchronize ou une utilisation inappropriée de Queue. Résultat : le Main Thread attend les workers, les workers attendent le Main Thread.

TThread.Queue vs. TThread.Synchronize : la différence pratique

Unschärfer Debugger-Blick mit Notizen zu Threads und Queue als Kontext für Queue vs Synchronize
Lors du débogage, on voit rapidement si les Worker-Threads attendent le Main Thread ou se contentent d’enregistrer des mises à jour dans la file (queued).

Les deux mécanismes servent à exécuter du code en toute sécurité sur le Main Thread. La différence tient à la sémantique d’attente :

  • TThread.Synchronize : le worker appelant attend que le Main Thread ait exécuté le code. C’est « synchrones », cela augmente les latences et constitue un ingrédient classique des deadlocks quand le Main Thread est bloqué.
  • TThread.Queue: Le worker place simplement le code dans une file d’attente pour le thread principal et poursuit son exécution. C’est « asynchrone », cela découple les threads et constitue dans les scénarios parallèles presque toujours le meilleur choix par défaut – à condition de maîtriser la fréquence des mises à jour.

Important: Queue n’est pas un laissez-passer. Si vous envoyez une mise à jour dans la Queue à chaque itération d’une boucle, vous inondez la file d’attente du thread principal. L’interface n’est alors pas bloquée par un deadlock, mais par la simple quantité de messages. L’interface paraît « lente », et la fin du traitement est retardée parce que des centaines ou des milliers de mises à jour de l’interface doivent encore être traitées.

Le cas limite qui fait vraiment mal : attente dans le thread de l’interface utilisateur

Dans les applications d’entreprise, on observe souvent la séquence suivante : clic sur le bouton « Start », interface désactivée, ouverture d’un ProgressDialog, puis attente synchrone jusqu’à la fin avant de réactiver. Ce schéma est à l’origine de nombreux deadlocks.

Variantes typiques (selon la base de code) :

  • Le thread principal lance TParallel.For puis appelle ensuite une logique d’attente bloquante (directement ou indirectement).
  • Un ProgressDialog appelle dans le constructeur ou dans OnShow une routine qui attend en interne.
  • Un bouton d’annulation met bien un flag, mais le thread principal reste néanmoins bloqué dans une boucle d’attente.

Si les worker-threads utilisent Synchronize pendant cette période, le deadlock est pratiquement garanti. Avec Queue, il peut aussi y avoir blocage si le thread principal est bloqué et ne traite pas les messages – car alors la Queue ne sera pas non plus vidée.

La conséquence opérationnelle est la suivante : le thread principal ne doit pas attendre de manière bloquante la fin de la boucle parallèle si des mises à jour de l’interface sont nécessaires en parallèle. À la place, le traitement doit soit être entièrement déplacé dans une tâche d’arrière-plan, soit on organise une « fin asynchrone » (callback/action de fin mise en file) qui rend l’interface à la fin.

Approche propre : progression uniquement agrégée, mises à jour de l’interface ralenties

Arbeitsplatzmotiv mit abstrahierter Progress-Anzeige und Zeitmessung als Symbol für gedrosselte UI-Updates
L’agrégation et la régulation temporelle empêchent que l’interface utilisateur soit inondée par trop de mises à jour.

Un modèle robuste repose sur trois responsabilités clairement séparées :

  • Worker-Threads effectuent le travail réel par élément/index. Ils ne signalent que la progression sous une forme sûre pour les threads (compteur, Queue, file d’attente thread-safe).
  • Aggregator (souvent : le thread principal ou un timer dédié dans l’interface utilisateur) calcule à partir de la progression un état pour l’interface (position, texte, ETA) et met à jour les contrôles. Cela évite des mises à jour 1:1 par itération.
  • Finalisation (également dans le thread principal) : réactiver l’interface, afficher le résultat, résumer les erreurs, libérer les ressources.

Pourquoi cette séparation fonctionne si bien : la charge de travail peut être à haute fréquence (des milliers d’éléments), alors que l’UI n’a besoin que de quelques mises à jour par seconde. En pratique, 5–10 mises à jour/s suffisent, pour des tâches très rapides même 2–4. Tout au‑delà constitue généralement du bruit visuel et coûte du temps CPU dans la boucle de messages.

Compter de manière thread‑sûre : atomique plutôt que verrou

Pour une ProgressBar simple, un compteur atomique suffit souvent. « Atomique » signifie : incrément et lecture se font sans race condition, typiquement via TInterlocked. Cela permet d’éviter des locks (sections critiques) dans le chemin critique de la boucle.

Idée minimale éprouvée :

  • La quantité totale est connue à l’avance (p. ex. nombre d’enregistrements, fichiers, IDs).
  • Chaque itération incrémente atomiquement un DoneCounter.
  • Un timer de l’UI lit périodiquement le compteur et définit ProgressBar.Position.

Avantage : pas de TThread.Queue par élément, pas d’inondation de l’UI. Inconvénient : pas de messages détaillés par élément (p. ex. nom de fichier). On peut toutefois ajouter un second message d’état ralenti (voir section suivante).

Messages d’état sans spam : « le dernier statut gagne »

Si vous souhaitez en plus afficher un court texte (élément courant, phase, message d’erreur), il faut aussi un schéma qui n’inonde pas l’UI à chaque étape du worker. En pratique, « le dernier statut gagne » fonctionne très bien :

  • Le worker écrit une information d’état dans une structure threadsûre (p. ex. une chaîne échangeable de façon atomique, ou protégée par un petit verrou).
  • Un timer de l’UI reprend périodiquement le dernier statut observé dans un label.

L’UI reste réactive et vous voyez néanmoins qu’« il se passe quelque chose ». Ce qui importe ici n’est pas tant la chaîne elle‑même que sa durée de vie : ne pas traîner des références vers des objets éphémères des threads worker dans le thread UI. Si vous transmettez des objets, clarifiez explicitement la propriété (ownership).

TParallel.For : UI de progression sécurisée pour les threads avec TThread.Queue — un modèle robuste

Il existe des scénarios où un timer UI seul ne suffit pas : p. ex. si vous voulez garantir de déclencher exactement une mise à jour « Terminé » à la fin, ou si la mise à jour UI est une opération plus complexe (p. ex. écrire dans une fenêtre de log, mais de manière ralentie). Dans ce cas, TThread.Queue est approprié — mais pas par itération, plutôt de façon ciblée.

Une approche pragmatique est d’avoir une file d’attente uniquement pour les événements à faible fréquence :

  • Start‑Event (préparer l’UI, verrouiller les boutons)
  • Événements de progression périodiques (max. toutes les X millisecondes)
  • Événements d’erreur (optionnellement collectés)
  • Done‑Event (réinitialiser l’UI, afficher le résultat)

La périodicité n’est pas contrôlée par le thread UI, mais déjà dans le contexte worker : le worker n’queue un update UI que si suffisamment de temps s’est écoulé depuis la dernière mise à jour UI. Une source de temps monotone convient (p. ex. TickCount) plus une valeur atomique de « last update ».

Important : la mise à jour UI elle‑même doit être « rapide ». Des calculs coûteux, I/O fichier ou accès base de données n’ont pas leur place dans le callback mis en queue. Le callback doit uniquement lire des états et définir des contrôles.

Cancel‑Handling : annuler sans blocage

Dans des applications réelles, l’annulation n’est pas optionnelle. L’essentiel : Cancel n’est pas un « kill », mais un arrêt coopératif. Les workers doivent vérifier régulièrement si un signal d’abandon est posé, puis sortir proprement. Dans Delphi il existe plusieurs voies pour cela (selon la construction PPL) : un drapeau Volatile dédié, un Boolean atomique, ou un concept d’annulation via Tasks (selon la version et la structure de Delphi).

Pour l’exploitation, deux règles sont importantes :

  • Le bouton Annuler doit être visible rapidement : Vérifiez le flag d’annulation à des endroits pertinents, pas seulement à la fin d’une itération, surtout si l’itération peut durer plusieurs secondes.
  • Le bouton Annuler doit effectuer le nettoyage : les handles ouverts, fichiers temporaires, transactions ou verrous ne doivent pas rester en suspens. Cela signifie : dans chaque itération de worker, des blocs try/finally sont obligatoires si des ressources sont impliquées.

Côté UI, le bouton Annuler ne devrait définir qu’un signal et mettre l’interface dans un état «Arrêt…». La terminaison effective et la réactivation de l’UI se font ensuite lors de l’événement Done, pas immédiatement au clic.

Éviter les deadlocks : les pièges les plus fréquents en pratique

Diagramme d'attente cyclique entre threads comme visualisation d'un deadlock
Les deadlocks résultent souvent d’une attente cyclique : le thread UI est bloqué, les workers attendent un accès à l’UI.

Piège 1 : WaitFor/Task.Wait dans le Main Thread

Si le Main Thread est bloqué, il ne peut pas exécuter de callbacks de Queue ni traiter de messages. Cela ressemble à un deadlock, même si les workers continuent de fonctionner correctement. Solution : pas d’attentes bloquantes dans le thread UI. Faire plutôt l’action de fin via TThread.Queue ou un pilotage par événement (p. ex. un timer qui vérifie «terminé»).

Piège 2 : Synchronize à l’intérieur d’un verrou

Un classique : un worker détient une critical section, appelle ensuite Synchronize, et dans le callback UI la même critical section est (directement ou indirectement) requise. Résultat : cycle d’attente. La règle est simple : ne pas transférer vers l’UI (Synchronize/Queue) depuis un verrou maintenu. Si un verrou est nécessaire, récupérez d’abord toutes les données dans des variables locales, libérez le verrou, puis effectuez le queue.

Piège 3 : le callback UI déclenche de la réentrance

Parfois, la mise à jour de l’UI n’est pas «inoffensive» : définir des propriétés peut déclencher des événements (OnChange, OnResize) qui à leur tour lancent de la logique accédant aux états des workers. Ce n’est pas un deadlock au sens strict, mais cela provoque des blocages difficiles à expliquer et des conditions de concurrence. Remède : placer les mises à jour UI dans des chemins «silencieux» (désactiver temporairement les événements) ou utiliser des garde-fous de réentrance (p. ex. un guard atomique pour la phase de mise à jour).

Piège 4 : trop de mises à jour en file

Même sans attentes, l’UI peut se figer si vous générez des dizaines de milliers de callbacks en file. Symptômes : la ProgressBar continue longtemps, la fenêtre répond lentement, CPU du Main Thread élevée. Solution : limiter (fenêtre temporelle), agréger (compteur), ou mettre en place une vraie structure producteur/consommateur où une seule mise à jour UI peut être «pending» (coalescing).

Quand c’est plus compliqué : collecter les résultats, agréger les erreurs, garantir les ordres

TParallel.For est idéal lorsque les itérations sont indépendantes. Dans le logiciel métier, les itérations sont souvent seulement «largement» indépendantes : elles lisent des fichiers, appellent des APIs REST, écrivent des lignes en base de données. Il faut alors planifier soigneusement trois points supplémentaires :

  • Collecte des résultats thread-safe : soit un buffer local par thread (fusionné à la fin), soit une queue/collection thread-safe. Éviter les locks dans le hot path.
  • Gestion des erreurs : les Exceptions provenant des Worker-Threads doivent être recueillies. En pratique, il est efficace de retenir la première Exception et de déclencher un Cancel, ou de collecter toutes les Exceptions et de les afficher regroupées à la fin.
  • Ordre : si la sortie nécessite un ordre stable (p. ex. des logs par index), le traitement parallèle avec un tri ultérieur est souvent plus simple que « insertion ordonnée thread-safe ».

Pour l’interface utilisateur, cela signifie : n’affichez pas chaque message d’erreur immédiatement. Cela conduit à une prolifération de boîtes de dialogue modales. Recueillez les erreurs (p. ex. une liste de chaînes) et affichez en fin une synthèse ou un log exportable.

Débogage : comment rendre l’interblocage réellement visible

Les interblocages dans du code parallèle sont frustrants, car ils peuvent apparaître différemment dans le débogueur par rapport à la version Release. Néanmoins, il existe quelques leviers pratiques :

Utiliser la fenêtre Threads et les piles d’appels

Si l’interface se fige, examinez tous les threads : où en est le Main Thread ? Attend-il ? Est-il dans une Message-Loop ? Où en sont les Worker-Threads ? Si des Workers sont bloqués dans Synchronize, la cause est presque toujours « Main Thread bloqué » ou « Main Thread attend un lock ».

Marquer les points Queue / Synchronize

Ajoutez du logging ciblé aux points de transfert (avant la Queue, dans le callback de la Queue, à la fin de l’itération). En production, c’est souvent plus utile que des points d’arrêt, car le timing est déterminant. Veillez à ce que le logging lui-même soit thread-safe et non bloquant (p. ex. pas de sortie de log vers l’UI directement depuis les Worker-Threads).

Mesurer le délai depuis la dernière mise à jour

Si l’interface « se fige », il se peut simplement qu’elle doive traiter trop d’updates. Mesurez donc dans le Main Thread la fréquence d’exécution des mises à jour UI par seconde et leur durée. Dès que les callbacks UI prennent plus que quelques millisecondes, il faut limiter la fréquence ou simplifier.

Quand TParallel.For avec Progress-UI est-il vraiment pertinent ?

La parallélisation n’est pas une fin en soi. Elle est particulièrement pertinente lorsque :

  • les itérations sont suffisamment longues (millisecondes à secondes), de sorte que la surcharge du thread pool est négligeable,
  • le travail est intensif CPU (parsing, compression, hashing) ou comporte des E/S facilement parallélisables (plusieurs fichiers, plusieurs requêtes HTTP avec limites),
  • vous disposez d’une stratégie claire d’annulation (Cancel) et de gestion des erreurs,
  • les exigences UI peuvent se contenter d’un progrès agrégé.

Elle est moins pertinente lorsque chaque itération est extrêmement courte (micro-opérations) ou quand toutes les itérations butent sur le même goulot d’étranglement (une transaction DB sérialisée, un verrou global, un fichier unique). Dans ce cas, les leviers les plus rapides sont souvent : améliorer l’algorithme, opérer en lots (batching), réduire l’accès aux données ou découpler explicitement le goulot.

Checklist pratique : maintenir la stabilité de l’UI

  • Le Main Thread ne doit pas être bloqué : pas d’attentes (Waits), pas de boucles longues sans Message Pump.
  • Les workers n’accèdent jamais aux Controls : pas d’accès VCL/FMX hors du UI-Thread.
  • Les mises à jour UI sont régulées : compteur/timer ou mises à jour via queue coalescée plutôt que par itération.
  • Pas d’appels Synchronize depuis l’intérieur de locks.
  • Le Cancel est coopératif, fréquemment vérifié et effectue un nettoyage correct.
  • Les Exceptions sont collectées et traitées de manière ordonnée à la fin.

Conclusion : découpler avec une Queue, stabiliser par agrégation

Une interface de progression robuste avec TParallel.For ne se crée pas en « mettre Synchronize n’importe où », mais par un principe d’architecture clair : les Worker travaillent de manière indépendante, le Main Thread reste libre et ne traite que quelques UI-Updates rapides. TThread.Queue est l’outil approprié lorsqu’il est utilisé de façon ciblée et régulée. En cas de comportement « lent » ou instable, deux causes sont presque toujours responsables : le Main Thread attend quelque part de manière bloquante – ou il est submergé par trop de mises à jour en file d’attente.

Une fois le modèle correctement mis en place (Counter/Coalescing, Cancel-Flag, callback de fin), il vaut la peine de l’appliquer à de nombreux endroits d’une application Delphi existante : import/export, validations de données, tâches fichier et jobs API – tout devient plus réactif, sans que vous vous exposiez à de nouveaux deadlocks à chaque Progress-Update.

Si vous avez besoin d’aide pour stabiliser du code parallèle, pour le debugging des blocages d’UI ou pour moderniser proprement des applications Delphi existantes : prenez contact.

Pour ce sujet, la Delphi Parallel Programming Library et Tthread.queue Vs Synchronize sont également importantes. L’article replace ces aspects de façon claire et montre ce qui compte au quotidien.

Discuter d’un projet ou d’une opération de modernisation avec Net-Base.

Étape suivante

Lorsque le sujet devient un projet réel, l'architecture, l'existant et l'exploitation doivent être examinés ensemble dès le départ.

Nous n'intervenons pas seulement sur des questions ponctuelles, mais aussi lorsque des fragments de code source, des problématiques liées aux systèmes legacy ou des concepts de portail doivent se transformer en un projet d'entreprise robuste.

  • L'état des lieux, l'état cible et les risques techniques sont évalués conjointement.
  • REST, l’accès aux données, les portails et le déploiement ne sont pas reportés à des phases ultérieures.
  • Vous identifiez tôt quelle voie est viable économiquement et opérationnellement.

Partager l'article

Partager directement cette publication

LinkedIn, X, XING, Facebook, WhatsApp et e‑mail sont immédiatement disponibles. Pour Instagram, nous préparons directement le lien et le court texte.

Courriel

Instagram s'ouvre dans un nouvel onglet. Le lien et le court texte sont préalablement copiés dans le presse-papiers.