Net-Base Revistă

24.07.2026

Delphi: TParallel.For cu UI de progres sigură pentru fire (TThread.Queue) fără blocaje

Cum să combinați Delphi TParallel.For cu o interfață de progres thread-safe: actualizări prin TThread.Queue, agregare curată, gestionarea anulării și capcane tipice de deadlock în depanare și în exploatare.

24.07.2026

De la tema din revistă la practica în proiecte

Pagini relevante de servicii și pagini tehnice pentru articol

Cine, în Delphi, vrea să paralelizeze joburi intensive din punct de vedere al calculului sau I/O ajunge rapid la Parallel Programming Library (PPL) și, în mod concret, la TParallel.For. Efectul se observă adesea imediat — până când, în aceeași clipă, vrei să actualizezi „pe fugă” o interfață de progres. Tocmai aici apar blocajele tipice: înghețări aparent aleatorii ale UI-ului, o ProgressBar care sare înapoi sau un deadlock complet atunci când rulezi pas cu pas în debugger.

În acest articol este vorba despre TParallel.For threadsichere Progress-UI: un model robust care funcționează în VCL și FMX, grupează curat actualizările UI prin TThread.Queue, ia în considerare Cancel/Abort și evită consecvent cele mai frecvente capcane de deadlock. Accentul este pe realitatea operațională: comportament reproductibil, responsabilități clare și indicații de debugging care ajută chiar și atunci când eroarea apare doar „la client”.

De ce eșuează atât de des UI-ul de progres cu TParallel.For

TParallel.For rulează, în mod tipic, pe worker-threads din threadpool-ul Delphi. Aceste thread-uri nu trebuie să acceseze direct controale VCL sau FMX, deoarece framework-urile UI (message loop, window handles, rendering) sunt legate de Main Thread. Chiar și o instrucțiune aparent inofensivă ProgressBar.Position := … dintr-un worker poate conduce la comportament nedefinit: AVs sporadice, ferestre înghețate sau actualizări „clipocitoare”.

Remediul evident este adesea TThread.Synchronize. Acesta rezolvă problema thread-safety, dar într-o buclă paralelă conduce rapid la altă problemă: creează un gât de sticlă serial. Fiecare worker așteaptă Main Thread, care la rândul său este ocupat cu randarea și executarea apelurilor Synchronize. Sub sarcină pare un deadlock — chiar dacă e „doar“ un efect de starvation/lockstep.

Și apoi există clasa de deadlock reală: Main Thread așteaptă (de ex. prin WaitFor, Task.Wait sau indirect prin apeluri blocante) sfârșitul operațiunii paralele, în timp ce worker-threads încearcă să trimită lucru în Main Thread prin Synchronize sau prin utilizarea nefavorabilă a Queue. Rezultat: Main Thread așteaptă worker-ele, worker-ele așteaptă Main Thread.

TThread.Queue vs. TThread.Synchronize: der praktische Unterschied

Privire neclară din debugger cu note despre thread-uri și Queue ca context pentru Queue vs Synchronize
La debugging se observă rapid dacă worker-threads așteaptă Main Thread sau doar trimit actualizări prin Queue.

Ambele mecanisme servesc la executarea în siguranță a codului pe Main Thread. Diferența este semantică în privința așteptării:

  • TThread.Synchronize: Worker-ul apelant așteaptă până când Main Thread execută codul. Acesta este „sincron”, crește latențele și reprezintă o cauză clasică a deadlock-urilor când Main Thread este blocat.
  • TThread.Queue: Worker plasează codul doar într-o coadă pentru Main Thread și continuă execuția. Das ist „asincron“, decuplează thread-urile și în scenarii paralele este aproape întotdeauna alegerea implicită mai bună – cu condiția să controlați frecvența actualizărilor.

Wichtig: Queue ist kein Freifahrtschein. Dacă trimiteți un update în coadă la fiecare iterație a unei bucle, veți supraîncărca coada Main Thread. Atunci UI-ul nu se blochează din cauza unui deadlock, ci din cauza cantității uriașe de mesaje. Interfața pare „zäh“, iar finalizarea procesării întârzie, pentru că încă rulează sute sau mii de actualizări UI.

Der Randfall, der wirklich wehtut: Warten im UI-Thread

În aplicațiile enterprise se observă frecvent următorul flux: se face click pe butonul „Start“, UI este dezactivată, apare ProgressDialog, apoi se „așteaptă“ sincron până se termină totul pentru ca ulterior să se reactiveze. Acest tipar este nucleul multor deadlocks.

Typische Varianten (je nach Codebasis):

  • Main Thread pornește TParallel.For și apoi apelează o logică de așteptare blocantă (direct sau indirect).
  • Un ProgressDialog apelează în Constructor sau OnShow o rutină care așteaptă intern.
  • Un buton de anulare setează un flag, dar Main Thread rămâne totuși blocat într-o buclă de așteptare.

Dacă worker-threads folosesc în acest timp Synchronize, deadlock-ul este practic garantat. Cu Queue se poate bloca de asemenea, dacă Main Thread este blocat și nu pompează mesaje – pentru că atunci nici coada nu este procesată.

Consecința robustă este: Main Thread nu trebuie să aștepte în mod blocant bucla paralelă, dacă sunt necesare actualizări UI paralele. În schimb, procesarea trebuie fie externalizată complet într-un task de fundal, fie se organizează un „sfârșit asincron“ (callback/acțiune de încheiere pusă în coadă), care reactivează UI la final.

Sauberer Ansatz: Progress nur aggregiert, UI-Updates gedrosselt

Motiv de loc de muncă cu afișare abstractizată a progresului și cronometrare ca simbol pentru actualizările UI limitate
Agregarea și limitarea bazată pe timp previn suprasolicitarea UI prin prea multe actualizări.

Un pattern robust constă din trei responsabilități clar separate:

  • Worker-Threads efectuează munca efectivă per element/index. Ei raportează doar progresul într-o formă sigură pentru thread-uri (contor, Queue, coadă thread-safe).
  • Aggregator (adesea: Main Thread sau un timer dedicat în UI) calculează din progres un status UI (poziție, text, ETA) și actualizează controalele. Astfel evitați actualizările 1:1 per iterație.
  • Finalizare (de asemenea în Main Thread): reactivare UI, afișare rezultat, sumarizare erori, eliberare resurse.

De ce această separare funcționează atât de bine: Workload-ul poate fi de înaltă frecvență (mii de elemente), dar UI-ul are nevoie doar de câteva actualizări pe secundă. În practică sunt suficiente 5–10 actualizări/secundă, la joburi foarte rapide chiar 2–4. Tot ce depășește asta este de regulă doar zgomot vizual și consumă timp CPU în bucla de mesaje.

Contorizare thread-safe: Atomic statt Lock

Pentru un ProgressBar simplu adesea este suficient un contor atomic. „Atomic” înseamnă: incrementarea și citirea se întâmplă fără condiții de cursă, tipic prin TInterlocked. Astfel evitați Locks (Critical Sections) în hot path-ul buclei.

Ideea minimală dovedită:

  • Cantitatea totală este cunoscută dinainte (de ex. număr de înregistrări, fișiere, ID-uri).
  • Fiecare iterație mărește atomic un DoneCounter.
  • Un UI-Timer citește periodic contorul și setează ProgressBar.Position.

Avantaj: Niciun TThread.Queue per element, nicio supraîncărcare a UI-ului. Dezavantaj: nu aveți mesaje detaliate per element (de ex. nume de fișier). Pentru asta se poate adăuga un al doilea mesaj de stare limitat (vezi secțiunea următoare).

Statusmeldungen ohne Spam: „letzter Status gewinnt“

Dacă doriți să afișați suplimentar un text scurt (element curent, fază, mesaj de eroare), este nevoie tot de un pattern care să nu inunde UI-ul la fiecare pas al worker-ului. În practică, „letzter Status gewinnt” funcționează foarte bine:

  • Worker-ul scrie o informație de stare într-o structură thread-safe (de ex. un string schimbabil atomic sau protejat printr-un lock mic).
  • UI-Timer-ul preia periodic ultimul status văzut într-un label.

Asta păstrează UI-ul reactiv și totuși vedeți că „se întâmplă ceva”. Mai important decât stringul în sine este durata de viață: nu aduceți referințe la obiecte efemere din worker-threads în UI-thread. Dacă transmiteți obiecte, clarificați ownership-ul în mod explicit.

TParallel.For threadsichere Progress-UI mit TThread.Queue: ein robustes Muster

Există scenarii în care un UI-Timer singur nu este suficient: de ex. când doriți la final să declanșați sigur exact un update „Finalizat”, sau când update-ul UI este un pas mai complex (de ex. o înscriere într-un fereastră de log, dar limitată). Atunci este potrivit TThread.Queue – însă nu per iterație, ci țintit.

O abordare practică este o coadă doar pentru evenimente cu frecvență scăzută:

  • Start-Event (pregătire UI, blocare butoane)
  • Evenimente periodice de progres (max. la fiecare X Millisekunden)
  • Fehler-Events (opțional colectate)
  • Done-Event (resetare UI, afișare rezultat)

Periodicitatea nu o obțineți în thread-ul UI, ci deja în contextul worker-ului: permiteți worker-ilor să facă queue unui UI-update doar dacă a trecut suficient timp de la ultimul UI-update. Pentru asta se potrivește o sursă de timp monotonă (de ex. TickCount) plus un atomic „last update”-valoare.

Important: update-ul UI în sine trebuie să fie „rapid”. Calculi costisitoare, I/O pe fișiere sau acces la baza de date nu au ce căuta în callback-ul UI pus în coadă. Callback-ul ar trebui doar să citească stări și să seteze controalele.

Cancel-Handling: Abbrechen ohne Hänger

În aplicații reale, anularea nu este opțională. Esențial este: Cancel nu este un „Kill”, ci o terminare cooperativă. Worker-ii trebuie să verifice regulat dacă un semnal de anulare este setat și apoi să iasă curat. În Delphi există mai multe căi pentru asta (în funcție de construcția PPL): un flag Volatile propriu, un boolean atomic, sau un concept de cancelare bazat pe Tasks (în funcție de versiunea și structura Delphi).

Pentru operare sunt importante două reguli:

  • Cancel trebuie să fie vizibil rapid: Verificați flag-ul de anulare în puncte potrivite, nu doar la sfârșitul unei iterații, dacă iterația poate dura și câteva secunde.
  • Cancel trebuie să curețe: Handles deschise, fișiere temporare, tranzacții sau locks nu trebuie să rămână. Asta înseamnă: În fiecare iterație a worker-ului sunt try/finally-blocuri obligatorii, când sunt implicate resurse.

Din partea UI, Cancel ar trebui să seteze doar un semnal și să pună UI într-o stare „Stopping…“. Închiderea efectivă și reactivarea UI au loc în Done-Event, nu imediat la clic.

Deadlocks vermeiden: die häufigsten Fallen in der Praxis

Diagramm eines zyklischen Wartens zwischen Threads als Visualisierung eines Deadlocks
Deadlock-urile apar adesea din cauza așteptării ciclice: UI-Thread este blocat, Worker-ii așteaptă acces la UI.

Falle 1: WaitFor/Task.Wait im Main Thread

Când Main Thread este blocat, nu poate executa callback-urile Queue și nu poate procesa mesaje. Aceasta seamănă cu un deadlock, chiar dacă worker-ii continuă să ruleze corect. Soluție: Fără așteptări blocante în UI-Thread. În schimb, acțiunea de finalizare prin TThread.Queue sau prin control de evenimente (de ex. un timer verifică „gata“).

Falle 2: Synchronize innerhalb eines Locks

Un clasic: Worker-ul deține o Critical Section, apoi apelează Synchronize, iar în callback-ul UI este (direct sau indirect) din nou necesară aceeași Critical Section. Rezultat: așteptare circulară. Regula e simplă: Nu efectuați transferul către UI (Synchronize/Queue) dintr-un lock deținut. Dacă e necesar un lock, preluați mai întâi toate datele în variabile locale, părăsiți lock-ul, apoi faceți queue.

Falle 3: UI-Callback triggert Reentrancy

Uneori actualizarea UI în sine nu este „inofensivă“: setarea proprietăților poate declanșa evenimente (OnChange, OnResize) care, la rândul lor, pornesc logică ce accesează stările worker-ilor. Aceasta nu este un deadlock în sens strict, dar duce la blocaje greu de explicat și la race conditions. Soluție: plasați update-urile UI pe căi „silencioase“ (dezactivați temporar evenimentele) sau utilizați reentrancy-guards (de ex. un guard atomic pentru faza de update).

Falle 4: Zu viele queued Updates

Chiar și fără waits, UI poate „îngheța“ dacă generați zeci de mii de callbacks queued. Simptome: ProgressBar continuă mult timp, fereastra răspunde lent, CPU în Main Thread ridicat. Soluție: limitare (ferestre de timp), agregare (Counter), sau o structură reală Producer/Consumer, unde doar un update UI poate fi „pending“ (Coalescing).

Când devine mai complicat: colectarea rezultatelor, gruparea erorilor, garantarea ordinii

TParallel.For este ideal când iterațiile sunt independente. În software-ul de business, iterațiile sunt însă adesea doar „în mare” independente: citiți fișiere, apelați REST-APIs, scrieți rânduri în baza de date. Atunci trebuie să planificați bine încă trei puncte:

  • Colectare thread-safe a rezultatelor: Fie pentru fiecare thread un buffer local (la final se reunesc) sau o coadă/colecție thread-safe. Evitați lock-urile pe hot path.
  • Gestionarea erorilor: Exceptions din thread-urile worker trebuie centralizate. În practică s-a dovedit util: rețineți prima Exception și declanșați Cancel, sau colectați toate Exceptions și afișați-le la final într-un singur raport.
  • Ordinea: Dacă ieșirea necesită o ordine stabilă (de ex. loguri după index), procesarea paralelă cu sortare ulterioară este adesea mai simplă decât „inserarea ordonată, sigură pentru thread-uri”.

Pentru UI asta înseamnă: nu afișați fiecare mesaj de eroare imediat. Aceasta duce la o avalanșă de dialoguri modale. Colectați erorile (de ex. o listă de Strings), și afișați la final un rezumat sau un log exportabil.

Debugging: Cum să faceți deadlock-ul cu adevărat vizibil

Deadlock-urile în cod paralel sunt frustrante, deoarece pot apărea în debugger diferit față de Release. Totuși există câteva pârghii foarte practice:

Utilizarea ferestrei Threads și a Call Stacks

Dacă UI se blochează, verificați toate thread-urile: unde se află Main Thread? Așteaptă? Se află într-o buclă de mesaje (Message-Loop)? Unde sunt Worker-Threads? Dacă worker-ii rămân blocați în Synchronize, cauza este aproape întotdeauna „Main Thread blocat” sau „Main Thread are nevoie de un lock”.

Marcarea punctelor Queue-/Synchronize

Plasați țintit Logging la punctele de transfer (înainte de Queue, în callback-ul Queue, la sfârșitul iterației). În funcționare asta este adesea mai valoros decât Breakpoints, pentru că timing-ul e decisiv. Asigurați-vă că Logging-ul în sine este thread-sigur și non-blocant (de ex. fără ieșire de log în UI direct din Worker-Threads).

Măsurați timpul ultimei actualizări

Dacă UI „se blochează”, poate pur și simplu să fie din cauza faptului că trebuie să proceseze prea multe actualizări. Măsurați în Main Thread cât de des pe secundă executați un UI-Update și cât timp durează. De îndată ce callback-urile UI necesită mai mult de câteva milisecunde, este necesară limitarea sau simplificarea.

Când merită cu adevărat TParallel.For cu Progress-UI?

Paralelizarea nu este un scop în sine. Este justificată în special când:

  • iterațiile sunt suficient de mari (Millisekunden bis Sekunden), astfel încât Threadpool-Overhead să fie neglijabil,
  • jobul este CPU-licit (Parsing, Kompression, Hashing) sau are I/O bine paralelizabil (mai multe fișiere, mai multe HTTP-Requests cu limite),
  • aveți o strategie clară de Cancel și de gestionare a erorilor,
  • cerințele UI pot funcționa cu un progres agregat.

Este mai puțin utilă când fiecare iterație este extrem de scurtă (Mikro-Operationen) sau când toate iterațiile se lovesc de același Engpass (o tranzacție DB serială, un lock global, un fișier unic). Atunci levierul mai rapid este adesea: îmbunătățirea algoritmului, procesarea în batch, reducerea accesului la date sau decuplarea explicită a blocajului.

Checklistă practică: Astfel rămâne UI stabilă

  • Main Thread nu este blocat: keine Waits, keine langen Loops ohne Message Pump.
  • Worker-ii nu ating niciodată Controls: niciun acces VCL/FMX în afara UI-Thread-ului.
  • UI-Updates sunt limitate: Counter/Timer sau coalesced Queue-Updates în loc de pe iterație.
  • Nicio apelare Synchronize din interiorul Locks.
  • Cancel este cooperativ, verificat frecvent și curăță resursele în mod ordonat.
  • Exceptions sunt colectate și tratate la final într-o manieră ordonată.

Concluzie: Decuplați cu Queue, stabilizați prin agregare

O interfață de progres robustă pentru TParallel.For nu apare prin „a băga Synchronize undeva“, ci printr-un principiu arhitectural clar: workerii lucrează independent, Main Thread rămâne liber și procesează doar câteva actualizări UI rapide. TThread.Queue este instrumentul corect atunci când îl folosiți țintit și limitat. Pentru comportamente „leneșe“ sau instabile sunt aproape întotdeauna două cauze: Main Thread așteaptă undeva blocant – sau este copleșit de prea multe actualizări puse în coadă.

Dacă ați implementat o dată modelul curat (Counter/Coalescing, Cancel-Flag, Abschluss-Callback), merită aplicat în multe locuri dintr-o aplicație Delphi matură: Import/Export, verificări de date, joburi de fișiere și API – toate devin mai receptive, fără să vă atrageți deadlock-uri noi la fiecare actualizare de progres.

Dacă aveți nevoie de asistență pentru stabilizarea codului paralel, pentru debuggingul blocajelor UI sau pentru modernizarea curată a aplicațiilor Delphi existente: contactați-ne.

Pentru acest subiect sunt relevante și Delphi Parallel Programming Library și Tthread.queue Vs Synchronize. Articolul ordonează aceste aspecte clar și arată ce contează în practică.

Discută proiectul sau planul de modernizare cu Net-Base.

Pasul următor

Dacă un subiect devine un proiect real, arhitectura, starea existentă și operarea ar trebui analizate împreună încă din faza incipientă.

Nu oferim sprijin doar pentru întrebări punctuale, ci și atunci când fragmente de cod sursă, probleme legacy sau idei de portal trebuie transformate într-un proiect robust la nivel de companie.

  • Situația curentă, starea țintă și riscurile tehnice sunt evaluate împreună.
  • REST, accesul la date, portalurile și implementarea nu sunt amânate pentru etape ulterioare.
  • Veți vedea din timp care opțiune este viabilă din punct de vedere economic și operațional.

Partajează postarea

Distribuiți această postare direct

LinkedIn, X, XING, Facebook, WhatsApp și E-Mail sunt disponibile imediat. Pentru Instagram pregătim direct linkul și textul scurt.

E-mail

Instagram se deschide într-o filă nouă. Linkul și textul scurt se copiază în prealabil în clipboard.