Net-Base Ajakiri

24.07.2026

Delphi: TParallel.For koos lõimeohutu progressi-UI-ga (TThread.Queue) ilma deadlock'ideta

Nii kombineerite Delphi TParallel.For-i lõimeturvalise edenemise kasutajaliidesega: TThread.Queue kaudu tehtavad uuendused, korrektne agregatsioon, tühistamise käsitlus ja tüüpilised deadlock-lõksud silumisel ja tootmises.

24.07.2026

Ajakirjateemast projektipraktikasse

Sobivad teenuse- ja tehnilised lehed postituse jaoks

Kes soovib Delphi arvutusintensiivseid või I/O-koormusega töid paralleelselt töödelda, satub kiiresti Parallel Programming Library (PPL) ja konkreetselt TParallel.For peale. Tulemused on tihti kohe mõõdetavad – kuni soovitakse samal ajal Progress-UI „mal eben“ värskendada. Just siin tekivad tüüpilised hangid: pealtnäha juhuslikud UI-külmumised, ProgressBar, mis hüppab tagasi, või täielik deadlock, kui debugeri abil samm-sammult läbi minnakse.

Selles artiklis käsitleme TParallel.For lõimiturvalist Progress-UI-d: robustset mustrit, mis töötab nii VCL-is kui FMX-is, koondab UI-uuendused üle TThread.Queue korrektselt, arvestab Cancel/Abort-i ja väldib järjekindlalt levinumaid deadlock-allsüsteeme. Fookus on töökeskkonna reaalsusel: reprodutseeritav käitumine, arusaadavad vastutused ja debugimisvihjed, mis aitavad ka siis, kui viga ilmneb ainult „kliendi juures“.

Miks Progress-UI TParallel.For puhul nii sageli ebaõnnestub

TParallel.For jookseb tavapäraselt worker-lõimedel, mis pärinevad Delphi-threadpool-ist. Need lõimed ei tohi otse VCL- või FMX-komponente mõjutada, sest UI-ramistikud (Message Loop, Window Handles, Rendering) on seotud pealõimega. Isegi näiliselt kahjutu ProgressBar.Position := … worker-lõimest võib viia määratlemata käitumiseni: perioodilised AV-d, akende külmumised või „vilkuvad“ uuendused.

Ilmselge paranduse tavaliselt pakub TThread.Synchronize. See küll taastab lõimiturvalisuse, kuid paralleelsetes tsüklites viib see kiiresti teise probleemini: tekib seriaalne kitsaskoht. Iga worker ootab UI-lõime, mis omakorda tegeleb renderdamise ja Synchronize-kutsete täitmisega. Koormuse all võib see välja näha nagu deadlock – isegi kui tegu on „ainult“ starvation/lockstep-efektiga.

On ka tõelise deadlocki klass: pealõim ootab (nt WaitFor, Task.Wait abil või kaudselt läbi blokeerivate kutsumiste) paralleeloperatsiooni lõppu, samal ajal kui worker-lõimed püüavad Synchronize või ebasoodsa Queue-kasutusega tööd pealõimele saata. Tulemuseks: pealõim ootab workereid, workerid ootavad pealõimet.

TThread.Queue vs. TThread.Synchronize: praktiline erinevus

Udune debuggivaade märkmetega lõimede ja Queue kohta kontekstina Queue vs Synchronize
Debugimisel näeb kiiresti, kas worker-lõimid ootavad pealõimet või ainult lisavad uuendusi järjekorda.

Mõlemad mehhanismid võimaldavad koodi turvaliselt pealõimes täita. Erinevus seisneb ootamise semantikas:

  • TThread.Synchronize: kutsuv worker ootab, kuni pealõim koodi käivitab. See on „sünkroonne“, suurendab latentsust ja on klassikaline komponent deadlock-ide tekkeks, kui pealõim on parasjagu blokeeritud.
  • TThread.Queue: Worker asetab koodi ainult Main Threadi ootesse ja jätkab tööd. See on „asünkroonne“, eraldab threadid ja on paralleelskenaarioides peaaegu alati parem vaikimisi valik – tingimusel, et värskendussagedust kontrollitakse.

Tähtis: Queue ei ole vaba pääs. Kui saadate iga tsükli iteratsiooni kohta ühe Queue-uuenduse, uputate Main-Thread-Queue’i. Sel juhul ei lukustu UI küll deadlock’i tõttu, kuid sõnumite tohutu hulgaga. UI tundub „jäik“ ja töötlemise lõpp lükkub edasi, sest veel sajad või tuhanded UI-uuendused jooksevad järgi.

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

Ettevõtte rakendustes nähakse sageli järgmist käiku: klõpsatakse nuppu „Start“, UI deaktiveeritakse, ProgressDialog avaneb ja seejärel oodatakse sünkroonselt kuni kõik on valmis, et seejärel UI uuesti aktiveerida. See muster on paljude deadlock’ide keskne põhjus.

Tüüpilised variandid (sõltuvalt koodibaasist):

  • 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.

Kui Worker-Threads sel ajal kasutavad Synchronize, on deadlock praktiliselt garanteeritud. Ka Queue võib kinni jääda, kui Main Thread on blokeeritud ega pumbata Messages – sest siis ei töödeldagi Queue’i.

Betriebsfeste järeldus on: Der Main Thread darf nicht blockierend auf die Parallel-Schleife warten, wenn parallel UI-Updates benötigt werden. Selle asemel tuleb töötlus kas täielikult tausttööks viia või korraldada „asünkroonne Ende“ (Callback/Queued Abschlussaktion), mis lõpuks UI taas vabastab.

Sauberer Ansatz: Progress nur aggregiert, UI-Updates gedrosselt

Töökoha motiiv abstraktse Progress-näidiku ja ajamõõtmisega, mis sümboliseerib piiratud UI-uuendusi
Agregatsioon ja aegpõhine piiramine takistavad UI ülekoormamist liiga paljude uuendustega.

Tugev muster koosneb kolmest selgelt eraldatud vastutusvaldkonnast:

  • Worker-Threads teevad tegeliku töö iga elemendi/indeksi kohta. Nad teatavad edusammudest ainult thread-ohutul kujul (loendur, Queue, Thread-safe Queue).
  • Aggregator (sageli: Main Thread oder ein dedizierter Timer im UI) arvutab edusammudest UI-seisu (Position, Text, ETA) ja uuendab kasutajaliidese elemente (Controls). Nii väldite 1:1-uuendusi iga iteratsiooni kohta.
  • Lõpetamine (samuti Main Threadis): UI taasaktiveerida, tulemused kuvada, vead koondada, ressursid vabastada.

Miks see eraldus nii hästi toimib: töökoormus võib olla kõrgsageduseline (tuhanded elemendid), kuid UI vajab vaid mõneid uuendusi sekundis. Praktiliselt piisab 5–10 uuendusest/sekundis, väga kiirete tööde puhul isegi 2–4. Kõik sellest rohkem on enamasti ainult visuaalne müra ja kulutab CPU-aega Message Pumpis.

Lõimturvaline loendamine: atomaarne, mitte lukustus

Lihtsa ProgressBar’i jaoks piisab tihti atomaarsest loendurist. „Atomaarne“ tähendab: inkrement ja lugemine toimuvad ilma race condition’ita, tavaliselt läbi TInterlocked. Sel viisil väldite lukke (Critical Sections) tsükli hot path’is.

Soovitatav minimaallahendus:

  • Kogumaht on eelnevalt teada (nt kirje-, faili- või ID-arv).
  • Iga iteratsioon suurendab atomaarselt DoneCounteri.
  • UI-Timer loeb periodiliselt counteri ja seab ProgressBar.Positioni.

Eelis: puudub TThread.Queue iga elemendi kohta, UI ei uputata. Puudus: teil ei ole detailseid sõnumeid iga elemendi kohta (nt failinimi). Selle kompenseerimiseks saab lisada teise, pingestatud staatusteate (vt järgmine jaotis).

Oleku teated ilma spammita: „viimane staatus loeb“

Kui soovite lisaks lühiteksti kuvada (aktiivne element, faas, veateade), on tarvis samuti mustrit, mis ei uputa UI-d iga worker-astme korral. Praktikas toimib hästi põhimõte „viimane staatus loeb“:

  • Worker kirjutab olekuinfo lõimiturvalisse struktuuri (nt atomaarne asendatav string või kaitstud väikese lukuga).
  • UI-Timer võtab periodiliselt viimati nähtud staatuse üle ja kuvab selle labelis.

Nii säilib UI reageerimisvõime ja samas on näha, et „midagi toimub“. Oluline on siin vähem stringi sisu kui selle elutsükkel: ärge viige UI-threadi viiteid lühiajalistele objektidele worker-thread’ist. Kui edastate objekte, täpsustage omandiõigus selgelt.

TParallel.For lõimiturvaline Progress-UI koos TThread.Queue: robustne muster

On stsenaariume, kus UI-Timer üksi ei piisa: nt kui soovite protsessi lõpus garanteerida täpselt ühe „valmis“ uuenduse või kui UI-uuendus on keerukam samm (nt kirje logiaknas, kuid piiratud sagedusega). Siis on TThread.Queue sobiv – kuid mitte iteratsiooni kohta, vaid sihipäraselt.

Praktiline lähenemine on järjekord ainult madala sagedusega sündmuste jaoks:

  • Start-Event (UI ettevalmistus, nupud lukku)
  • Perioodilised progress-event’id (kuni üks kord iga X millisekundi järel)
  • Vea-event’id (valikuline, kogutud)
  • Done-Event (UI reset, tulemuse kuvamine)

Periodilisuse saavutate mitte UI-thread’i peal, vaid juba worker-kontekstis: workerid queue’divad UI-uuenduse ainult siis, kui piisavalt aega on möödunud viimase UI-uuenduseni. Selleks sobib monotoonne ajaloomeeter (nt TickCount) koos atomaarse „last update“ väärtusega.

Tähtis: UI-uuendus ise peab olema „kiire“. Kallid arvutused, faili-I/O või andmebaasioperatsioonid ei kuulu gequeu’itud UI-callback’i. Callback peab ainult olekuid lugema ja kontrollereid seadistama.

Cancel-Handling: katkestamine ilma hangumiseta

Tegelikus rakenduses ei ole katkestamine valikuline. Otsustav on: Cancel ei ole „Kill“, vaid koostööl põhinev lõpetamine. Worker’id peavad regulaarselt kontrollima, kas katkestussignaal on seatud, ja siis korrektselt väljuma. In Delphi on selleks mitu võimalust (sõltuvalt PPL-konstruktsioonist): oma Volatile-lipp, atomaarne Boolean või Tasks-põhine Cancellation-mudel (sõltuvalt Delphi versioonist ja arhitektuurist).

Töö käigus on olulised kaks reeglit:

  • Cancel peab kiiresti nähtav olema: Kontrollige katkestuslipu olekut mõistlikel kohtadel, mitte ainult iteratsiooni lõpus, kui iteratsioon võib kesta ka sekundeid.
  • Cancel peab koristama: Avatud Handles, ajutised failid, transaktsioonid või lukud ei tohi jääda alles. See tähendab: iga Worker-iteratsiooni juures on try/finally-plokid kohustuslikud, kui ressursid on seotud.

UI-poolt peaks Cancel ainult signaali seadma ja UI viima olekusse „Stopping…“. Tegelik lõpetamine ja UI taasaktiveerimine toimuvad Done-Event’is, mitte kohe klõpsu hetkel.

Deadlockide vältimine: praktilised levinumad lõksud

Diagramm eines zyklischen Wartens zwischen Threads als Visualisierung eines Deadlocks
Deadlockid tekivad sageli tsüklilise ootamise tõttu: UI-Thread blokeeritud, Worker ootavad UI-juurdepääsu.

Lõks 1: WaitFor/Task.Wait Main-Thread’is

Kui Main-Thread on blokeeritud, ei saa ta täita Queue-callback’e ega töödelda sõnumeid. See näib deadlock’ina, isegi kui Worker’id edasi töötavad. Lahendus: ärge kasutage blokeerivaid Wait-e UI-thread’is. Selle asemel tehke lõpetustoiming TThread.Queue kaudu või sündmusepõhine juhtimine (nt taimer kontrollib „fertig“/„valmis“).

Lõks 2: Synchronize lukus olles

Klassika: Worker hoiab Critical Section’i, kutsub seejärel Synchronize ja UI-callback’is on (otse või kaudselt) taas vaja sama Critical Section’i. Tulemuseks on ringiline ootamine. Reegel on lihtne: UI-ülekandeid (Synchronize/Queue) ei tohi teha hoitud luku sees. Kui lukk on vajalik, looge kõik andmed esmalt kohalikesse muutujatesse, vabastage lukk ja seejärel pange need järjekorda.

Lõks 3: UI-callback käivitab reentrantsi

Mõnikord ei ole UI-uuendus iseenesest „ohutu“: omaduste (Properties) seadmine võib vallandada event’e (OnChange, OnResize), mis omakorda käivitavad loogika, mis pääseb Worker-olekutele ligi. See ei ole kitsamas mõttes deadlock, kuid põhjustab raskesti seletatavaid külmumisi ja race condition’e. Lahendused: viige UI-uuendused „vaiksetesse“ rajadesse (deaktiveerige event’id ajutiselt) või kasutage reentrancy-guard’e (nt atomaarne guard uuendusfaasi jaoks).

Lõks 4: Liiga palju järjekorda pandud uuendusi

Isegi ilma Wait-eita võib UI „seisma jääda“, kui genereerida kümneid tuhandeid queued callback’e. Sümptomid: ProgressBar liigub kaua peale, aken reageerib aeglaselt, Main-Thread’i CPU-koormus kõrge. Lahendus: tômmake (time-window), agreggeerige (loendur) või kasutage tõelist Producer/Consumer-struktuuri, kus korraga võib olla vaid üks ootel UI-uuendus (coalescing).

Kui asi muutub keerulisemaks: tulemuste kogumine, vigade koondamine, järjekorra garanteerimine

TParallel.For on ideaalne, kui iteratsioonid on sõltumatud. Äri-tarkvaras on iteratsioonid aga sageli vaid „suures osas“ sõltumatud: loete faile, kutsute REST-APIsid, kirjutate andmebaasiridasid. Sel juhul peate selgelt planeerima kolm lisapunkti:

  • Lõim‑ohutu tulemuste kogumine: kas iga lõim kasutab kohalikku puhvrid (lõpus kokku liita) või kasutatakse lõim‑ohutut järjekorda/kogumit. Lukke hot path’is vältida.
  • Vea käsitlemine: Worker-Threadidest pärinevad erandid tuleb kokku korjata. Praktikas on tõhus: meeles pidada esimene erand ja algatada tühistamine, või koguda kõik erandid ja kuvada need lõpus koondatult.
  • Järjestus: Kui väljund peab olema stabiilse järjekorraga (nt logid indeksi järgi), on paralleeltöötlus hilisema sorteerimisega sageli lihtsam kui „lõimeturvaline järjestatud lisamine“.

UI jaoks tähendab see: ärge kuvage iga üksikut veateadet koheselt. See viib modaalsete dialoogide põrgusse. Koguge vead (nt stringide nimekiri) ja kuvage lõpus kokkuvõte või eksporditav logi.

Debugimine: kuidas deadlocki tõeliselt nähtavaks teha

Deadlockid paralleelses koodis on frustreerivad, sest debuggeris võivad need välja näha teisiti kui release-versioonis. Siiski on mõned praktilised võtted:

Threads-akna ja call stackide kasutamine

Kui UI on kinni, vaadake kõiki threade: kus on Main Thread? Kas ta ootab? Kas ta on sõnumitsüklis? Kus paiknevad worker-threadid? Kui worker-threadid jäävad kinni Synchronize‚i, on põhjus peaaegu alati „Main Thread blokeeritud“ või „Main Thread vajab lukku“.

Queue-/Synchronize-kohad märgistamine

Pange sihipäraselt logimine üleandmiskohtadele (enne järjekorda, järjekorra callback’is, iteratsiooni lõpus). Käigus on see sageli väärtuslikum kui breakpoint’id, sest timing on määrav. Veenduge, et logimine ise oleks lõimeturvaline ja mitteblokkeeriv (nt mitte kuvada UI-logi otse worker-threadidest).

Viimase värskenduse aja mõõtmine

Kui UI „hangib“, võib põhjus olla lihtsalt selles, et tal on liiga palju värskendusi töödelda. Mõõtke seetõttu Main Thread’is, mitu UI-värskendust sekundis teete ja kui kaua igaüks kestab. Kui UI-callback’id võtavad rohkem kui paar millisekundit, on vaja piirata või lihtsustada.

Millal tasub TParallel.For koos Progress-UI-ga tõesti kasutada?

Paralleeltöötlemine ei ole eesmärk omaette. See tasub end eriti, kui:

  • iteratsioonid on piisavalt suured (millisekunditest sekunditeni), nii et threadpooli üldkulu muutub ebaoluliseks,
  • ülesanne on CPU-intensiivne (parsimine, kompressioon, hashimine) või omab hästi paralleelset I/O-d (mitu faili, mitu HTTP-päringut koos limiitidega),
  • teil on selge tühistamise ja veastrateegia,
  • UI-nõuded saab katta koondatud progressiga.

See tasub end vähem, kui iga iteratsioon on äärmiselt lühike (mikrooperatsioonid) või kui kõik iteratsioonid läbivad sama kitsaskoha (järjestikune DB-tehing, globaalne lukustus, üksik fail). Sel juhul on tihti kiireima efektiga lähenemine: algoritmi parandamine, partiitöö (batching), andmepääsu vähendamine või kitsaskoha selge eraldamine.

Praktiline kontrollnimekiri: nii jääb UI stabiilseks

  • Main Thread ei blokeeru: mitte ooteoperatsioonid ega pikad silmused ilma sõnumipumbata.
  • Worker-threadid ei puutu kunagi kontrollidesse: VCL/FMX-i juurdepääs peab toimuma ainult UI-threadis.
  • UI-värskendused on piiratud: loendur/timer või koondatud järjekorravärskendused asemel värskendust iga iteratsiooni kohta.
  • Ei ole Synchronize-kutseid lockide sees.
  • Tühistamine on kooperatiivne, seda kontrollitakse sageli ja see puhastab ressursid korrektselt ära.
  • Erandeid kogutakse ja töödeldakse lõpus korrapäraselt.

Kokkuvõte: eraldage järjekorraga, stabiliseerige koondamisega

Eine robuste Progress-UI bei TParallel.For entsteht nicht durch „irgendwo Synchronize rein“, sondern durch ein klares Architekturprinzip: Worker arbeiten unabhängig, der Main Thread bleibt frei und verarbeitet nur wenige, schnelle UI-Updates. TThread.Queue ist dabei das richtige Werkzeug, wenn Sie es gezielt und gedrosselt einsetzen. Für „zähes“ oder instabiles Verhalten sind fast immer zwei Ursachen verantwortlich: der Main Thread wartet irgendwo blockierend – oder er ertrinkt in zu vielen queued Updates.

Wenn Sie das Muster einmal sauber aufgesetzt haben (Counter/Coalescing, Cancel-Flag, Abschluss-Callback), lohnt es sich über viele Stellen in einer gewachsenen Delphi-Anwendung hinweg: Import/Export, Datenprüfungen, Datei- und API-Jobs – alles wird reaktionsfähiger, ohne dass Sie sich bei jedem Progress-Update neue Deadlocks einhandeln.

Wenn Sie Unterstützung beim Stabilisieren von Parallel-Code, beim Debugging von UI-Hängern oder beim sauberen Modernisieren gewachsener Delphi-Anwendungen brauchen: Kontakt aufnehmen.

Für dieses Thema sind auch Delphi Parallel Programming Library und Tthread.queue Vs Synchronize wichtig. Der Beitrag ordnet diese Aspekte verständlich ein und zeigt, worauf es im Alltag ankommt.

Projekt oder Modernisierungsvorhaben mit Net-Base besprechen.

järgmine samm

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

Me ei toeta ainult üksikute küsimuste lahendamist, vaid ka siis, kui lähtekoodilõikudest, pärandsüsteemidest või portaalikontseptsioonidest peab saama usaldusväärne ettevõtteprojekt.

  • Olemasolev olukord, sihtpilt ja tehnilised riskid hinnatakse üheskoos.
  • REST, Datenzugriff, Portale und Rollout werden nicht als Spätfolgen verschoben.
  • Te näete varakult, milline tee on majanduslikult ja operatiivselt jätkusuutlik.

Jaga postitust

Jaga seda postitust otse

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

e-post

Instagram avatakse uues vahekaardis. Link ja lühitekst kopeeritakse eelnevalt lõikepuhvrisse.