Del tema de la revista a la pràctica del projecte
Pàgines de serveis i tècniques pertinents per a l'article
Qui a Delphi vol paralel·litzar tasques intensives de càlcul o amb molta E/S, arriba ràpidament a la Parallel Programming Library (PPL) i, concretament, a TParallel.For. L’efecte sovint és immediatament mesurable — fins que, al mateix temps, es vol «actualitzar» la Progress‑UI. Precisament aquí apareixen els bloquejos típics: congelacions de la UI aparentment aleatòries, una ProgressBar que salta cap enrere o un deadlock complet quan s’executa pas a pas amb el depurador.
En aquest article es tracta de TParallel.For threadsichere Progress-UI: un patró robust que funciona tant a VCL com a FMX, que agregue netament les actualitzacions de la UI mitjançant TThread.Queue, té en compte Cancel/Abort i esquiva de manera consistent les caixes negres de deadlock més habituals. El focus està en la realitat operativa: comportament reproductible, responsabilitats clares i consells de depuració que ajuden fins i tot quan l’error només passa «al client».
Per què la Progress-UI amb TParallel.For falla tan sovint
TParallel.For s’executa típicament en worker‑threads del threadpool de Delphi. Aquests fils no poden accedir directament als controls de VCL o FMX, perquè els frameworks de la UI (Message Loop, Window Handles, Rendering) estan lligats al Main Thread. Fins i tot un aparentment inofensiu ProgressBar.Position := … des d’un worker pot provocar comportaments indefinits: AVs esporàdics, finestres congelades o actualitzacions «parpellejants».
La solució evident sovint és TThread.Synchronize. Això soluciona la seguretat de fils, però en bucles paral·lels condueix ràpidament a un altre problema: es crea un coll d’ampolla serial. Cada worker espera el Main Thread, que al seu torn està ocupat en renderitzar i en processar crides de Synchronize. Sota càrrega això sembla un deadlock —encara que sigui només un efecte de starvation/lockstep.
I després està la classe de deadlock real: el Main Thread espera (per exemple via WaitFor, Task.Wait o indirectament per crides bloquejants) la finalització de l’operació paral·lela, mentre els worker‑threads intenten enviar feina al Main Thread mitjançant Synchronize o per una utilització desafortunada de Queue. Resultat: el Main Thread espera els workers i els workers esperen el Main Thread.
TThread.Queue vs. TThread.Synchronize: la diferència pràctica
Ambdós mecanismes serveixen per executar codi de manera segura al Main Thread. La diferència està en la semàntica d’espera:
- TThread.Synchronize: el worker que crida espera fins que el Main Thread ha executat el codi. Això és «síncron», augmenta la latència i és un ingredient clàssic per a deadlocks quan el Main Thread està bloquejat.
- TThread.Queue: El Worker posa el codi només en una cua per al Main Thread i continua executant-se. Això és «asíncron», desacobla threads i en escenaris de paral·lelisme és gairebé sempre l’opció per defecte més adequada — sempre que es controli la freqüència d’actualització.
Important: Queue no és un passaport lliure. Si en cada iteració d’un bucle envieu una actualització a la Queue, sobrecarregareu la Main-Thread-Queue. Llavors la UI no quedarà penjada per deadlock, però sí per la pura quantitat de missatges. La UI semblarà «lenteta», i la finalització del procés es retardarà perquè encara s’estan processant centenars o milers d’actualitzacions d’UI.
El cas límit que realment perjudica: esperar al UI-Thread
En aplicacions empresarials sovint es veu el següent flux: es fa clic al botó «Start», la UI es desactiva, s’obre un ProgressDialog, després s’«espera» de forma sincrònica fins que tot acabi i a continuació es torna a activar. Aquest patró és el nucli de molts deadlocks.
Variants típiques (segons la base de codi):
- Main Thread inicia TParallel.For i després crida una lògica d’espera bloquejant (directa o indirectament).
- Un ProgressDialog crida al constructor o a OnShow una rutina que internament espera.
- Un botó Cancel·lar estableix una bandera, però el Main Thread segueix quedant-se atrapat en un bucle d’espera.
Si els Worker-Threads fan servir Synchronize durant aquest temps, el deadlock està pràcticament garantit. Amb Queue també pot penjar-se si el Main Thread està bloquejat i no processa missatges — perquè llavors tampoc s’executarà la Queue.
La conseqüència operativa és: El Main Thread no ha d’esperar de forma bloquejant la bucle paral·lela si es necessiten actualitzacions d’UI en paral·lel. En comptes d’això, el processament ha d’externalitzar-se completament a una tasca en segon pla, o cal organitzar un «final asíncron» (callback/acció de finalització en cua) que torni a alliberar la UI al final.
Enfocament net: Progrés només agregat, actualitzacions d’UI regulades
Un patró robust consta de tres responsabilitats clarament separades:
- Worker-Threads fan la feina real per element/index. Només informen del progrés en una forma segura per a threads (comptador, Queue, Thread-safe Queue).
- Aggregator (sovint: el Main Thread o un temporitzador dedicat a la UI) calcula a partir del progrés un estat per la UI (posició, text, ETA) i actualitza els controls. Així eviteu actualitzacions 1:1 per iteració.
- Finalització (també en el Main Thread): reactivar la UI, mostrar el resultat, resumir errors, alliberar recursos.
Per què aquesta separació funciona tan bé: la càrrega de treball pot ser d’alta freqüència (milers d’elements), però la UI només necessita unes quantes actualitzacions per segon. A la pràctica n’hi ha prou amb 5–10 actualitzacions/segon, i en tasques molt ràpides fins i tot 2–4. Tot el que sobrepassa això acostuma a ser només soroll òptic i consumeix temps de CPU a la Message Pump.
Comptatge segur per a fils: atòmic en lloc de bloqueig
Per a una ProgressBar senzilla acostuma a ser suficient un comptador atòmic. «Atòmic» vol dir: l’increment i la lectura es fan sense Race Condition, típicament mitjançant TInterlocked. Així s’eviten locks (Critical Sections) al hot path del bucle.
Idea mínima provada:
- La quantitat total és coneguda prèviament (p. ex. nombre de registres, fitxers, IDs).
- Cada iteració incrementa un DoneCounter de manera atòmica.
- Un UI-Timer llegeix periòdicament el comptador i ajusta ProgressBar.Position.
Avantatge: cap TThread.Queue per element, no s’inunda la UI. Inconvenient: no disposeu de missatges de detall per element (p. ex. nom de fitxer). Per això es pot afegir un segon missatge d’estat limitat (vegeu la secció següent).
Missatges d’estat sense spam: „l’últim estat guanya“
Si a més voleu mostrar un text curt (element actual, fase, missatge d’error), cal també un patró que no inundi la UI a cada pas del worker. A la pràctica funciona molt bé «l’últim estat guanya»:
- El worker escriu una informació d’estat en una estructura segura per a fils (p. ex. un String intercanviable de manera atòmica, o protegit per un petit bloqueig).
- Un UI-Timer adopta periòdicament l’estat vist més recent en un Label.
D’aquesta manera la UI es manté reactiva i, alhora, veieu que «passa alguna cosa». Aquí és menys important el String en si que la seva durada de vida: no traslladeu referències a objectes efimers dels worker-threads al UI-thread. Si passeu objectes, clarifiqueu explícitament la propietat (ownership).
TParallel.For: Progress-UI segur per a fils amb TThread.Queue: un patró robust
Hi ha escenaris en què un UI-Timer no és suficient: p. ex. quan voleu assegurar-vos d’executar exactament una actualització de «Fet» al final, o quan l’actualització de la UI és un pas més complex (p. ex. un registre en una finestra de log, però limitat). En aquests casos TThread.Queue és adequat — però no per iteració, sinó de manera selectiva.
Un enfoc pràctic és una queue només per a esdeveniments de baixa freqüència:
- Start-Event (preparar la UI, bloquejar botons)
- Progress-Events periòdics (com a màxim cada X mil·lisegons)
- Error-Events (opcionalment agrupats)
- Done-Event (restablir la UI, mostrar el resultat)
La periodicitat no l’aconseguiu des del UI-thread, sinó ja des del context del worker: només deixeu que els workers facin queue d’una actualització de la UI si ha transcorregut prou temps des de l’última actualització. Per a això és útil una font de temps monòtona (p. ex. TickCount) més un valor atòmic de «last update».
Important: l’actualització de la UI ha de ser «ràpida». Càlculs costosos, I/O de fitxers o accessos a bases de dades no han d’anar al callback encolat a la UI. El callback hauria només de llegir estats i ajustar controls.
Gestió del cancel·lament: abortar sense que l’aplicació es penji
En aplicacions reals, l’abort no és opcional. El punt clau: Cancel no és un «kill», sinó una finalització cooperativa. Els workers han de comprovar periòdicament si s’ha establert un senyal d’abort i, en cas afirmatiu, sortir netament. A Delphi hi ha diverses maneres de fer-ho (segons la construcció PPL): un flag Volatile propi, un boolean atòmic, o un concepte de cancel·lació basat en Tasks (segons la versió i l’estructura de Delphi).
Per al funcionament són importants dues regles:
- Cancel muss schnell sichtbar werden: Comproveu el flag d’abort en punts raonables, no només al final d’una iteració, quan la iteració pot durar segons.
- Cancel muss aufräumen: Handles oberts, fitxers temporals, transaccions o Locks no poden quedar oberts. Això vol dir: en cada iteració del Worker són obligatoris blocs try/finally si hi ha recursos implicats.
A nivell d’UI, Cancel només hauria de posar un senyal i situar la UI en un estat «Stopping…». La finalització real i la reactivació de la UI han d’ocórrer en el Done-Event, no immediatament en el clic.
Evitar deadlocks: les trampes més freqüents a la pràctica
Trampa 1: WaitFor/Task.Wait al fil principal
Si el fil principal està bloquejat, no pot executar callbacks de Queue ni processar missatges. Això es comporta com un deadlock, encara que els Worker continuïn correctament. Solució: evitar waits bloquejants al fil de la UI. En comptes d’això, fer l’acció de finalització via TThread.Queue o mitjançant una lògica d’esdeveniments (p. ex. un timer que comprovi «fertig»).
Trampa 2: Synchronize dins d’un Lock
Un clàssic: un Worker manté una secció crítica, crida Synchronize, i en el callback de la UI es requereix (directa o indirectament) la mateixa secció crítica. Resultat: espera circular. La regla és senzilla: No transferir a la UI (Synchronize/Queue) des d’un Lock mantingut. Si necessiteu un Lock, llegiu totes les dades a variables locals, sortiu del Lock i després feu el queue.
Trampa 3: el callback de la UI provoca reentrància
De vegades l’actualització de la UI no és «inofensiva»: assignar propietats pot disparar esdeveniments (OnChange, OnResize) que al seu torn arranquen lògica que accedeix a estats dels Worker. Això no és un deadlock en sentit estricte, però provoca bloquejos difícils d’explicar i condicions de carrera. Mesures: fer les actualitzacions de la UI per rutes «silencioses» (desactivar temporalment els events) o utilitzar Reentrancy-Guards (p. ex. un guard atòmic per a la fase d’actualització).
Trampa 4: massa actualitzacions en cua
Fins i tot sense waits la UI pot «aturar-se» si genereu desenes de milers de callbacks en cua. Símptomes: la ProgressBar continua corrent molt de temps, la finestra respon amb retard, la CPU del fil principal puja. Solució: limitar (finestra temporal), agregar (contador), o una veritable estructura productor/consumidor, on només pugui haver un UI-update pendent (Coalescing).
Quan la cosa es complica: recollir resultats, agrupar errors, garantir seqüències
TParallel.For és ideal quan les iteracions són independents. En software empresarial les iteracions sovint només són «en gran part» independents: llegiu fitxers, crideu REST-APIs, escriviu línies a la base de dades. En aquests casos cal planificar amb cura tres punts addicionals:
- Recollida de resultats segura per a threads: o bé un buffer local per fil (fusionar al final) o una Queue/Collection segura per a threads. Eviteu locks al hot path.
- Gestió d’errors: cal recollir les excepcions dels worker-threads. A la pràctica funciona bé: recordar la primera excepció i activar la cancel·lació, o bé acumular totes les excepcions i mostrar-les resumidament al final.
- Ordre: si la sortida necessita un ordre estable (per ex. registres ordenats per índex), processar en paral·lel i després ordenar sovint és més senzill que fer una «inserció ordenada segura per fils».
Per a la UI això vol dir: no mostreu cada missatge d’error immediatament. Això condueix a un infern de quadres de diàleg modals. Recolliu els errors (per ex. una llista de cadenes) i mostreu al final un resum o un registre exportable.
Depuració: Com fer realment visible un interbloqueig
Els interbloqueigs en codi paral·lel són frustrants perquè en el depurador poden aparèixer diferent que en la versió Release. Tot i això, hi ha algunes palanques molt pràctiques:
Utilitzar la finestra de fils i les piles de crides
Si la UI es queda penjada, inspeccioneu tots els fils: on està el fil principal? Està esperant? És en un bucle de missatges? On estan els worker-threads? Si els workers estan bloquejats a Synchronize, la causa gairebé sempre és «fil principal bloquejat» o «el fil principal necessita un lock».
Marcar punts de Queue/Synchronize
Col·loqueu logging de manera selectiva als punts de transferència (abans de la Queue, al callback de la Queue, al final de la iteració). En execució això és sovint més valuós que els punts d’interrupció, perquè el timing és determinant. Assegureu-vos que el logging sigui thread-safe i no bloquejant (per ex. evitar sortides de registre a la UI directament des dels worker-threads).
Mesurar el temps de l’última actualització
Si la UI «es queda penjada», pot ser simplement que ha de processar massa actualitzacions. Mesureu per tant en el fil principal quantes actualitzacions d’IU feu per segon i quant triguen. Quan els callbacks de la IU necessiten més de pocs mil·lisegons, cal limitar o simplificar-les.
Quan compensa realment TParallel.For amb una Progress-UI?
La paral·lelització no és un fi en si mateixa. Compensa especialment quan:
- les iteracions són prou grans (milisegons a segons), de manera que l’overhead del pool de fils queda amortitzat,
- la tasca és CPU-intensiva (parsing, compressió, hashing) o té I/O fàcilment paral·lelitzable (diversos fitxers, diverses peticions HTTP amb límits),
- teniu una estratègia clara de cancel·lació i d’errors,
- els requisits de la UI es poden satisfer amb progressos agregats.
No compensa tant quan cada iteració és extremadament curta (microoperacions) o quan totes les iteracions impacten el mateix coll d’ampolla (una transacció BD sèria, un bloqueig global, un únic fitxer). En aquests casos el palanca més ràpida sovint és: millorar l’algorisme, agrupar en lots, reduir l’accés a dades o desacoblar explícitament el coll d’ampolla.
Checklist pràctic: Com mantenir la UI estable
- El fil principal no està bloquejat: no fer waits ni bucles llargs sense enganxar la Message Pump.
- Els workers no toquen mai Controls: cap accés a VCL/FMX fora del fil d’IU.
- Les actualitzacions de la IU estan limitades: comptadors/temporitzadors o actualitzacions acumulades en una cua en lloc d’una per iteració.
- No fer crides a Synchronize des de dins de locks.
- La cancel·lació és cooperativa, es comprova freqüentment i neteja correctament.
- Les excepcions es recullen i es processen ordenadament al final.
Conclusió: Desacoblar amb Queue, estabilitzar amb agregació
Una robusta interfície d’estat de progrés amb TParallel.For no s’aconsegueix posant simplement „Synchronize“ en algun lloc, sinó amb un principi d’arquitectura clar: els workers treballen de manera independent, el fil principal queda lliure i processa només unes poques actualitzacions d’UI ràpides. TThread.Queue és l’eina adequada, si l’utilitzeu de manera selectiva i amb control del ritme. Per a comportaments „lents“ o inestables gairebé sempre hi ha dues causes: el fil principal espera bloquejat en algun lloc – o s’ofega amb massa actualitzacions en cua.
Si configureu aquest patró correctament (comptador/coalescència, flag de cancel·lació, callback de finalització), val la pena aplicar-lo en molts punts d’una aplicació Delphi consolidada: importació/exportació, comprovacions de dades, treballs de fitxers i d’API – tot esdevé més reactiu sense que hagueu d’afrontar nous deadlocks a cada actualització de progrés.
Si necessiteu suport per estabilitzar codi paral·lel, per depurar bloquejos de la UI o per modernitzar de manera neta aplicacions Delphi existents: Poseu-vos en contacte.
Per a aquest tema són també importants la Delphi Parallel Programming Library i Tthread.queue Vs Synchronize. Aquest article situa aquests aspectes de manera clara i mostra què és rellevant en la pràctica diària.
Pas següent
Wenn aus dem Thema ein reales Projekt wird, sollten Architektur, Bestand und Betrieb früh zusammen betrachtet werden.
No només donem suport en qüestions puntuals, sinó també quan, a partir de fragments de codi font, temes de sistemes heredats o idees de portal, ha de sorgir un projecte empresarial sòlid.
- L'estat actual, la visió objectiu i els riscos tècnics s'avaluen conjuntament.
- REST, Datenzugriff, Portale und Rollout werden nicht als Spätfolgen verschoben.
- Veu aviat quin camí és viable des del punt de vista econòmic i operatiu.