Net-Base Žurnalas

24.07.2026

Delphi: TParallel.For su gijų saugia pažangos vartotojo sąsaja (TThread.Queue) be užstrigimų

Kaip sujungti Delphi TParallel.For su gijoms saugia progreso UI: atnaujinimai per TThread.Queue, tvarkingas agregavimas, atšaukimo valdymas ir tipiškos deadlock spąstys derinant ir eksploatuojant.

24.07.2026

Nuo žurnalo temos iki projekto įgyvendinimo

Tinkami puslapiai apie paslaugas ir techninę informaciją šiam įrašui

Kas nori Delphi skaičiavimo intensyvias arba I/O intensyvias užduotis vykdyti lygiagrečiai, greitai patenka prie Parallel Programming Library (PPL) ir konkrečiai prie TParallel.For. Efektas dažnai matomas iš karto – kol tame pačiame žingsnyje norima „greitai“ atnaujinti progreso sąsają. Būtent čia atsiranda tipiniai stabtelėjimai: tariamai atsitiktiniai UI-Freezes, ProgressBar, kuri šoka atgal, arba visiškas Deadlock, kai debugeriu einama žingsnis po žingsnio.

Šiame straipsnyje nagrinėjama TParallel.For gijų saugi Progress-UI: patikimas šablonas, veikiantis tiek su VCL, tiek su FMX, tvarkingai sujungiantis UI atnaujinimus per TThread.Queue, atsižvelgiantis į Cancel/Abort ir nuosekliai vengiant dažniausių deadlock spąstų. Dėmesys skiriamas eksploatacinei realybei: atkuriamam elgesiui, aiškioms atsakomybėms ir derinimo nurodymams, kurie padeda net tada, kai klaida pasireiškia tik „pas klientą“.

Kodėl Progress-UI su TParallel.For taip dažnai nepavyksta

TParallel.For paprastai vykdomas ant worker gijų, paimtų iš Delphi-gijų baseino. Šios gijos neturi tiesiogiai liesti VCL ar FMX valdiklių, nes UI karkasai (Message Loop, Window Handles, Rendering) yra susieti su pagrindine gija. Net tariamai nekaltas ProgressBar.Position := … iš worker gali sukelti nenuoseklų elgesį: epizodines AV, užšalusius langus arba „mirgančius“ atnaujinimus.

Natūrali pataisa dažnai yra TThread.Synchronize. Tai išsprendžia gijų saugumą, bet lygiagrečiose cikluose greitai sukelia kitą problemą: susidaro serijinis siauras taškas. Kiekviena worker gija laukia pagrindinės gijos, kuri tuo metu užsiėmusi renderiavimu ir Synchronize kvietimų vykdymu. Po apkrovos tai atrodo kaip deadlock – net jei iš tikrųjų tai „tik“ starvation/lockstep efektas.

Yra ir tikroji deadlock klasė: pagrindinė gija laukia (pvz. per WaitFor, Task.Wait arba netiesiogiai per blokuojančius kvietimus) kol baigsis lygiagretinė operacija, tuo tarpu worker gijos bando per Synchronize arba netinkamą Queue naudojimą perduoti darbą pagrindinei gijai. Rezultatas: pagrindinė gija laukia worker, worker gijos laukia pagrindinės gijos.

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

Unschärfer Debugger-Blick mit Notizen zu Threads und Queue als Kontext für Queue vs Synchronize
Derinant matosi greitai, ar worker gijos laukia pagrindinės gijos, ar tik deda queued atnaujinimus.

Abu mechanizmai skirti saugiai vykdyti kodą pagrindinėje gijoje. Skirtumas yra laukimo semantikoje:

  • TThread.Synchronize: Iškviečiantis worker laukia, kol pagrindinė gija įvykdys kodą. Tai yra „sinchroninis“ veiksmas, didina latentiją ir yra klasikinė deadlock priežastis, kai pagrindinė gija yra užblokuota.
  • TThread.Queue: Der Worker stellt den Code nur in eine Warteschlange für den Main Thread und läuft weiter. Das ist „asynchron“, entkoppelt Threads und ist in Parallel-Szenarien fast immer die bessere Default-Wahl – sofern man die Update-Frequenz kontrolliert.

Wichtig: Queue ist kein Freifahrtschein. Wenn Sie in jeder Iteration einer Schleife ein Queue-Update schicken, überfluten Sie die Main-Thread-Queue. Dann hängt die UI zwar nicht wegen Deadlock, aber wegen schierer Menge an Nachrichten. Die UI wirkt „zäh“, und das Ende der Verarbeitung verzögert sich, weil noch hunderte oder tausende UI-Updates nachlaufen.

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

In Unternehmensanwendungen sieht man häufig folgenden Ablauf: Button „Start“ klickt, UI wird deaktiviert, ProgressDialog geht auf, dann wird synchron „gewartet“, bis alles fertig ist, um anschließend wieder zu aktivieren. Dieses Muster ist der Kern vieler Deadlocks.

Typische Varianten (je nach Codebasis):

  • Main Thread startet TParallel.For und ruft danach eine blockierende Wartelogik auf (direkt oder indirekt).
  • Ein ProgressDialog ruft im Constructor oder OnShow eine Routine auf, die intern wartet.
  • Ein Abbrechen-Button setzt zwar ein Flag, aber der Main Thread bleibt trotzdem in einem Warteloop hängen.

Wenn Worker-Threads in dieser Zeit Synchronize benutzen, ist der Deadlock praktisch garantiert. Mit Queue kann es ebenfalls hängen, wenn der Main Thread blockiert und keine Messages pumpt – denn dann wird auch die Queue nicht abgearbeitet.

Die betriebsfeste Konsequenz lautet: Der Main Thread darf nicht blockierend auf die Parallel-Schleife warten, wenn parallel UI-Updates benötigt werden. Stattdessen muss die Verarbeitung entweder vollständig in einen Hintergrund-Task ausgelagert werden, oder man organisiert ein „asynchrones Ende“ (Callback/Queued Abschlussaktion), das die UI am Schluss wieder freigibt.

Sauberer Ansatz: Progress nur aggregiert, UI-Updates gedrosselt

Arbeitsplatzmotiv mit abstrahierter Progress-Anzeige und Zeitmessung als Symbol für gedrosselte UI-Updates
Agregacija ir laiko pagrindu atliekamas ribojimas neleidžia UI būti užplūstai per daug atnaujinimų.

Tvirtas modelis susideda iš trijų aiškiai atskirtų atsakomybių:

  • Worker-Threads atlieka faktinį darbą kiekvienam elementui/indeksui. Jie praneša tik apie pažangą saugia dauggijų forma (skaitiklis, Queue, thread-safe Queue).
  • Aggregator (dažnai: Main Thread oder ein dedizierter Timer im UI) apskaičiuoja iš progreso UI būseną (pozicija, tekstas, ETA) und atnaujina valdiklius. Taip išvengiate 1:1-Updates pro Iteration.
  • Abschluss (ebenfalls im Main Thread): UI vėl aktyvuoti, rezultatą parodyti, klaidas apibendrinti, resursus atlaisvinti.

Kodėl toks atskyrimas gerai veikia: darbo krūvis gali būti labai dažnas (tūkstančiai elementų), tačiau UI reikia tik kelių atnaujinimų per sekundę. Praktikoje užtenka 5–10 atnaujinimų/s, labai greitiems darbams net 2–4. Viskas, kas viršija tai, dažniausiai yra tik optinis triukšmas ir kainuoja CPU laiką Message Pump.

Saugus skaičiavimas kelių gijų atveju: Atomic vietoj Lock

Paprastai paprastai ProgressBar užtenka atomarinio skaitiklio. „Atominis“ reiškia: inkrementavimas ir skaitymas vyksta be lenktyninės sąlygos, tipinė realizacija — per TInterlocked. Taip išvengsite Locks (Critical Sections) karštajame ciklo kelyje.

Išbandyta minimali idėja:

  • Bendras kiekis žinomas iš anksto (pvz., įrašų, failų, ID skaičius).
  • Kiekviena iteracija atomariškai padidina DoneCounter.
  • UI-Timer periodiškai skaito skaitiklį ir nustato ProgressBar.Position.

Privalumas: nėra TThread.Queue už kiekvieną elementą, nėra UI perkrovos. Trūkumas: neturite detalių pranešimų už kiekvieną elementą (pvz., failo pavadinimo). Tam galima pridėti antrą, sulėtintą statuso žinutę (žr. kitą skyrių).

Statuso pranešimai be šlamšto: „paskutinis statusas laimi“

Jei norite papildomai rodyti trumpą tekstą (dabar apdorojamas elementas, fazė, klaidos pranešimas), reikalingas modelis, kuris neflood’intų UI kiekviename Worker žingsnyje. Praktikoje labai gerai veikia „paskutinis statusas laimi“:

  • Worker įrašo statuso informaciją į daugiagiję saugią struktūrą (pvz., atomariškai keičiamiem String arba apsaugotą mažo Lock’o).
  • UI-Timer periodiškai perima paskutinį matytą statusą į label.

Taip UI lieka reaguojanti ir vis tiek matote, kad kažkas vyksta. Svarbesnė čia ne pats stringas, o jo galiojimo trukmė: neperneškite nuorodų į trumpalaikius objektus iš Worker gijų į UI giją. Jei perduodate objektus, aiškiai apibrėžkite jų ownership.

TParallel.For kelių gijų saugi Progress-UI su TThread.Queue: patikimas šablonas

Yra situacijų, kai UI-Timer vienas neužtenka: pvz., kai pabaigoje reikia garantuotai iškviesti vieną „Fertig“ atnaujinimą, arba kai UI atnaujinimas yra sudėtingesnis žingsnis (pvz., įrašas į log langą, bet reguliuojamai). Tada TThread.Queue yra tinkamas — bet ne už kiekvieną iteraciją, o tik tikslingai.

Praktiškai tinkamas požiūris yra Queue tik žemų dažnių įvykiams:

  • Start-Event (paruošti UI, užrakinti mygtukus)
  • Periodiški Progress-Events (maks. kas X milisekundžių)
  • Klaidos-Event’ai (neprivaloma, surenkami)
  • Done-Event (atstatyti UI, parodyti rezultatą)

Periodiką užtikrinate ne UI gijoje, o Worker kontekste: leiskite Worker queue’inti UI atnaujinimą tik tada, kai nuo paskutinio UI atnaujinimo praėjo pakankamai laiko. Tam tinka monotoniškas laiko šaltinis (pvz., TickCount) kartu su atomariška „last update“ reikšme.

Svarbu: pats UI-Update turi būti „greitas“. Brangios skaičiavimo operacijos, failų I/O ar duomenų bazės užklausos neturi būti gequeue’intame UI callback’e. Callback’as turėtų tik nuskaityti būsenas ir nustatyti Controls.

Cancel-Handling: nutraukimas be užstrigimų

Realiuose sprendimuose nutraukimas nėra neprivalomas.Esminis principas: Cancel nėra „Kill“, o kooperatyvus užbaigimas. Worker turi reguliariai tikrinti, ar nustatytas nutraukimo signalas, ir tada tvarkingai išeiti. Visiškai tinkami keli sprendimo būdai yra aprašyti Delphi (priklausomai nuo PPL-Konstruktion): atskiras Volatile-flagas, atomarinis Boolean arba Cancellation-konceptas per Tasks (priklausomai nuo Delphi versijos ir struktūros).

Eksploatacijai svarbios dvi taisyklės:

  • Cancel muss schnell sichtbar werden: Tikrinkite nutraukimo žymą tinkamose vietose, ne tik iteracijos pabaigoje, jei iteracija gali užtrukti kelias sekundes.
  • Cancel muss aufräumen: Atidarių handle‘ų, laikinų failų, transakcijų ar lock‘ų negalima palikti. Tai reiškia: kiekvienoje Worker-iteracijoje privalomi try/finally-blokai, jei dalyvauja resursai.

UI pusėje Cancel turėtų tik nustatyti signalą ir perkelti UI į „Stopping…“ būseną. Tikrasis užbaigimas ir UI reaktivavimas turi vykti Done-Event įvykyje, o ne iškart paspaudimo metu.

Deadlock’ų vengimas: dažniausios spąstai praktikoje

Diagrama, vaizduojanti ciklišką laukimą tarp siūlų kaip deadlock'o vizualizacija
Deadlock’ai dažnai kyla dėl cikliško laukimo: UI siūlas blokuojamas, Worker laukia prieigos prie UI.

Spąstai 1: WaitFor/Task.Wait pagrindiniame siūle

Jei pagrindinis siūlas blokuojamas, jis negali vykdyti Queue-callback’ų ir apdoroti žinučių. Tai atrodo kaip deadlock’as, net jei Worker’ai teisingai toliau veikia. Sprendimas: vengti blokuojančių laukimų UI siūle. Vietoje to pabaigos veiksmą vykdykite per TThread.Queue arba per įvykio valdymą (pvz., timer tikrina „baigta“).

Spąstai 2: Synchronize užrakto viduje

Klasika: Worker laiko Critical Section, tada kviečia Synchronize, o UI-callback’e (tiesiogiai arba netiesiogiai) vėl reikalinga ta pati Critical Section. Rezultatas: cikliškas laukimas. Taisyklė paprasta: iš laikomo lock’o neiškvieskite UI-perdavimų (Synchronize/Queue). Jei lock’as būtinas, pirmiausia paimkite visus duomenis į vietines kintamąsias, išeikite iš lock’o ir tik tada įdėkite į Queue.

Spąstai 3: UI-Callback sukelia reentrancy

Kartais pats UI atnaujinimas nėra „nekaltas“: savybių nustatymas gali sukelti įvykius (OnChange, OnResize), kurie vėl paleidžia logiką, prieinančią prie Worker būsenų. Tai nėra deadlock’as griežtąja prasme, bet sukelia sunkiai paaiškinamus užstrigimus ir race conditions. Sprendimas: perkelti UI atnaujinimus į „tylius“ kelius (laikinai išjungti įvykius) arba naudoti reentrancy-guard’us (pvz., atominį guard’ą atnaujinimo fazei).

Spąstai 4: Per daug queued atnaujinimų

Net be laukimų UI gali „sustingti“, jei sukuriate dešimtis tūkstančių queued callback’ų. Simptomai: ProgressBar ilgai atnaujina, langas reaguoja vangiai, pagrindinio siūlo CPU apkrova didelė. Sprendimas: daryti throttle (laiko langas), agreguoti (skaitiklis) arba naudoti tikrą Producer/Consumer struktūrą, kurioje gali būti tik vienas laukiantis UI atnaujinimas (coalescing).

Kai tampa sudėtingiau: rezultatų rinkimas, klaidų sujungimas, eiliškumo garantavimas

TParallel.For yra idealus, kai iteracijos nepriklausomos. Tačiau verslo programinėje įrangoje iteracijos dažnai tik „iš esmės“ nepriklausomos: jos skaito failus, kviečia REST-APIs, rašo duomenų bazės eiles. Tokiu atveju turite tris papildomus klausimus kruopščiai suplanuoti:

  • Kelių siūlų saugi rezultatų kaupimas: arba kiekvienam siūlui vietinis buferis (pabaigoje sujungiamas), arba siūlams saugi Queue/Collection. Venkite lock’ų karštajame kelyje.
  • Klaidų tvarkymas: Exceptions iš Worker-Threads reikia surinkti. Praktikoje pasiteisina: užfiksuoti pirmąją Exception ir inicijuoti Cancel, arba surinkti visas Exceptions ir pabaigoje jas parodyti apibendrintai.
  • Tvarka: Jei išvestis reikalauja stabilią tvarką (pvz., žurnalai pagal indeksą), paralelinis apdorojimas su vėlesniu surikiavimu dažnai yra paprasčiau nei „siūlams saugus tvarkingas įterpimas“.

UI atžvilgiu tai reiškia: nerodykite kiekvienos klaidos pranešimo iš karto. Tai veda į modalių dialogų pragarą. Rinkite klaidas (pvz., eilutų sąrašą) ir pabaigoje parodykite santrauką arba eksportuojamą žurnalo failą.

Debugging: Kaip tikrai padaryti deadlock matomą

Deadlockai paraleliniame kode yra frustruojantys, nes debugeryje gali atrodyti kitaip nei release versijoje. Vis dėlto yra keli labai praktiški svertai:

Threads-Fenster und Call Stacks nutzen

Jei UI užstringa, peržiūrėkite visus siūlus: kur stovi Main Thread? Ar jis laukia? Ar jis Message-Loop? Kur stovi Worker-Threads? Jei Worker užstringa Synchronize, priežastis beveik visada yra „Main Thread blockiert“ arba „Main Thread braucht einen Lock“.

Queue-/Synchronize-Stellen markieren

Įdėkite tikslingą loggingą perėmimo vietose (prieš Queue, Queue-callback’e, iteracijos pabaigoje). Veikime tai dažnai yra vertingiau nei Breakpoints, nes laikas yra lemiamas. Užtikrinkite, kad logging pats būtų thread-safe ir neblokinis (pvz., jokios UI-log išvesties tiesiai iš Worker-Threads).

Last-Update-Zeit messen

Jei UI „užstringa“, gali būti paprasčiausia priežastis — per daug atnaujinimų apdorojama. Matuokite Main Thread, kiek kartų per sekundę atliekate UI-atnaujinimą ir kiek laiko tai užtrunka. Kai UI-callback’ai ima užtrukti daugiau nei kelis milisekundus, reikia daryti drosselinimą arba supaprastinimą.

Kada TParallel.For su Progress-UI iš tikrųjų apsimoka?

Paralelizacija nėra savitikslis. Ji ypač apsimoka, kai:

  • iteracijos yra pakankamai ilgos (milisekundės iki sekundžių), kad Threadpool-Overhead taptų nereikšmingas,
  • užduotis yra CPU-sunkioji (Parsing, Kompression, Hashing) arba turi gerai paralelizuotą I/O (kelios bylos, keli HTTP-Requests su ribojimais),
  • turite aiškią Cancel- ir klaidų strategiją,
  • UI reikalavimai patenkinti agreguotu progress.

Ji mažiau apsimoka, jei kiekviena iteracija yra labai trumpa (mikrooperacijos) arba jei visos iteracijos remiasi tuo pačiu siauru tašku (seriškas DB-transakcija, globalus lock, viena byla). Tokiu atveju greitesnis sprendimas dažnai yra: pagerinti algoritmą, daryti batch’us, sumažinti duomenų prieigą arba aiškiai atskirti perkrovos vietą.

Praktinis kontrolinis sąrašas: Kaip UI išlieka stabili

  • Main Thread neturi būti blokuojamas: jokio Wait, jokių ilgų ciklų be Message Pump.
  • Worker niekada neliečia Controls: jokio VCL/FMX-prieigos už UI-Thread ribų.
  • UI-atnaujinimai yra drosselinti: Counter/Timer arba coalesced Queue-ataiškinimai vietoje atnaujinimo po kiekvienos iteracijos.
  • Nėra Synchronize-kvietimų i61 Locks.
  • Cancel yra kooperatyvus, dažnai tikrinamas ir tvarkingai atlieka valymą.
  • Exceptions surenkamos ir pabaigoje tvarkingai apdorojamos.

Išvada: Atsieti su Queue, stabilizuoti per agregaciją

Atspari progreso vartotojo sąsaja su TParallel.For neatsiranda vien įkėlus „Synchronize“ kažkur, o dėl aiškaus architektūrinio principo: workeriai veikia nepriklausomai, pagrindinis siūlas lieka laisvas ir apdoroja tik kelis greitus UI atnaujinimus. TThread.Queue yra tinkamas įrankis, jei jį naudojate tikslingai ir dozuotai. Dėl „lėto“ arba nestabilaus elgesio beveik visada atsakingos dvi priežastys: pagrindinis siūlas kažkur blokuoja laukdamas – arba jis skęsta per daug eiliuotų atnaujinimų.

Kai modelis vieną kartą tvarkingai įdiegtas (Counter/Coalescing, Cancel-Flag, užbaigimo callback), jis atsiperka daugelyje vietų išaugusioje Delphi aplikacijoje: importas/eksportas, duomenų patikros, failų ir API uždaviniai – viskas pradeda reaguoti greičiau, be to, jums nereikės kiekvieno progreso atnaujinimo metu susikurti naujų deadlock’ų.

Jei jums reikia pagalbos stabilizuojant paralelinį kodą, derinant UI užstrigimus arba tvarkingai modernizuojant išaugusias Delphi aplikacijas: susisiekite.

Šiai temai taip pat svarbios Delphi Parallel Programming Library ir Tthread.queue Vs Synchronize. Straipsnis aiškiai išdėsto šiuos aspektus ir parodo, kas svarbu kasdieniame darbe.

Aptarkite projektą arba modernizacijos užmojus su Net-Base.

Sekantis žingsnis

Wenn aus dem Thema ein reales Projekt wird, sollten Architektur, Bestand und Betrieb früh zusammen betrachtet werden.

Mes padedame ne tik pavienėse užklausose, bet ir tuomet, kai iš šaltinio kodo fragmentų, paveldėtų temų ar portalo idėjų turi tapti patikimas įmonės projektas.

  • Esama padėtis, tikslinis vaizdas ir techninės rizikos vertinami kartu.
  • REST, Datenzugriff, Portale und Rollout werden nicht als Spätfolgen verschoben.
  • Jūs anksti matote, kuris kelias yra ekonomiškai ir įmonės veiklos požiūriu tvarus.

Pasidalinti įrašu

Tiesiogiai pasidalinti šiuo įrašu

LinkedIn, X, XING, Facebook, WhatsApp und E-Mail sind sofort verfügbar. für Instagram bereiten wir Link und Kurztext direkt vor.

El. paštas

Instagram atidaromas naujame skirtuke. Nuoroda ir trumpas tekstas iš anksto nukopijuojami į iškarpinę.