Net-Base Magazín

24.07.2026

Delphi: TParallel.For s vláknovo bezpečným Progress-UI (TThread.Queue) bez deadlockov

Takto skombinujete Delphi TParallel.For s vláknovo bezpečným Progress-UI: aktualizácie cez TThread.Queue, čistá agregácia, obsluha zrušenia a typické pasce deadlocku pri ladení a prevádzke.

24.07.2026

Od témy magazínu k projektovej praxi

Súvisiace stránky služieb a technológií k príspevku

Kto chce v Delphi paralelizovať výpočtovo náročné alebo I/O-náročné úlohy, rýchlo narazí na Parallel Programming Library (PPL) a konkrétne na TParallel.For. Efekt je často okamžite merateľný – až kým sa v tom istom dychu nesnažíte „rýchlo“ aktualizovať Progress-UI. Práve tu sa objavujú typické zaseknutia: zdanlivo náhodné zamrznutia UI, ProgressBar, ktorá skáče späť, alebo kompletný deadlock, keď v debuggeri idete krok za krokom.

Tento článok sa zaoberá vláknovo-bezpečnou Progress-UI pre TParallel.For: robustný vzor, ktorý funguje vo VCL aj FMX, konsoliduje aktualizácie UI cez TThread.Queue, zohľadňuje Cancel/Abort a dôsledne obchádza najčastejšie pasce vedúce k deadlocku. Zameranie je na prevádzkovú realitu: reprodukovateľné správanie, zrozumiteľné zodpovednosti a tipy na ladenie, ktoré pomôžu aj vtedy, keď sa chyba prejaví len „u zákazníka“.

Prečo Progress-UI pri TParallel.For tak často zlyháva

TParallel.For typicky beží na worker-threadoch z Delphi-threadpoolu. Tieto threaddy nesmú priamo pristupovať k VCL- alebo FMX-ovými ovládacím prvkom, pretože UI frameworky (message loop, window handles, rendering) sú naviazané na Main Thread. Aj zdanlivo neškodné ProgressBar.Position := … z workeru môže viesť k nedefinovanému správaniu: sporadické AV, zamrznuté okná alebo „blikajúce“ aktualizácie.

Očividným riešením je často TThread.Synchronize. To síce vyrieši thread-safety, ale v paralelných slučkách rýchlo vytvorí iný problém: vznikne sériový úzky hrdlo. Každý worker čaká na UI-thread, ktorý je zároveň zaneprázdnený renderovaním a spracovaním Synchronize-volaní. Pod záťažou to vyzerá ako deadlock – hoci ide o efekt starvation/lockstep.

A potom existuje skutočná trieda deadlockov: Main Thread čaká (napr. cez WaitFor, Task.Wait alebo nepriamo cez blokujúce volania) na dokončenie paralelnej operácie, zatiaľ čo worker-thready sa pokúšajú poslať prácu do Main Threada prostredníctvom Synchronize alebo nevhodného použitia Queue. Výsledok: Main Thread čaká na workerov, workeri čakajú na Main Thread.

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
Pri debugovaní rýchlo zistíte, či worker-thready čakajú na Main Thread alebo len odosielajú queued aktualizácie.

Oba mechanizmy slúžia na bezpečné vykonanie kódu v Main Threade. Rozdiel je v semantike čakania:

  • TThread.Synchronize: Volajúci worker čaká, kým Main Thread vykoná kód. Je to „synchronné“, zvyšuje latenciu a je klasickou zložkou vedúcou k deadlockom, ak je Main Thread práve zablokovaný.
  • TThread.Queue: Worker umiestni kód len do fronty pre hlavné vlákno a pokračuje v práci. To je „asynchrónne“, oddeľuje vlákna a v paralelných scenároch je takmer vždy lepšia predvolená voľba – za predpokladu, že kontrolujete frekvenciu aktualizácií.

Dôležité: Queue nie je povolenie na bezobmedzené používanie. Ak pri každej iterácii slučky odošlete aktualizáciu do Queue, preplníte frontu hlavného vlákna. UI potom síce nezamŕza kvôli deadlocku, ale kvôli obrovskému počtu správ. UI pôsobí pomaly a necitlivo a dokončenie spracovania sa oneskorí, pretože sa ešte doháňa stovky alebo tisíce aktualizácií UI.

Okrajový prípad, ktorý naozaj škodí: čakanie v UI vlákne

V podnikových aplikáciách sa často vyskytuje nasledujúci postup: kliknutie na tlačidlo „Start“, deaktivovanie UI, otvorenie ProgressDialog a potom sa synchronne „čaká“, kým bude všetko hotové, aby sa následne UI znovu aktivovalo. Tento vzorec je jadrom mnohých deadlockov.

Typické varianty (v závislosti od kódovej bázy):

  • Hlavné vlákno spustí TParallel.For a potom volá blokujúcu čakaciu logiku (priamo alebo nepriamo).
  • ProgressDialog v Constructor alebo OnShow zavolá rutinu, ktorá interne čaká.
  • Tlačidlo Zrušiť síce nastaví príznak, ale hlavné vlákno zostane napriek tomu v čakacej slučke.

Ak Worker-Threads v tomto čase používajú Synchronize, deadlock je prakticky zaručený. S Queue to tiež môže zablokovať, ak je hlavné vlákno zablokované a nepúšťa správy – v takom prípade sa aj Queue nevybavuje.

Prevádzkový záver znie: Hlavné vlákno nesmie blokujúco čakať na paralelnú slučku, ak sú súčasne potrebné aktualizácie UI. Spracovanie musí byť buď úplne presunuté do background tasku, alebo je potrebné zorganizovať „asynchrónny koniec“ (callback/queued dokončovacia akcia), ktorý UI na konci opätovne uvoľní.

Rozumný prístup: priebeh agregovaný, aktualizácie UI obmedzené

Arbeitsplatzmotiv mit abstrahierter Progress-Anzeige und Zeitmessung als Symbol für gedrosselte UI-Updates
Agregácia a časové obmedzovanie zabraňujú preťaženiu UI príliš mnohými aktualizáciami.

Robustný vzorec pozostáva z troch jasne oddelených zodpovedností:

  • Worker-Threads vykonávajú samotnú prácu pre každý prvok/index. Hlásia len priebeh v thread-safe forme (čítač, Queue, thread-safe Queue).
  • Aggregator (často: hlavné vlákno alebo dedikovaný timer v UI) vypočíta z priebehu stav UI (pozícia, text, ETA) a aktualizuje ovládacie prvky. Tým sa vyhnete 1:1 aktualizáciám pre každú iteráciu.
  • Abschluss (tiež v hlavnom vlákne): znova aktivovať UI, zobraziť výsledok, zhrnúť chyby, uvoľniť zdroje.

Prečo toto oddelenie tak dobre funguje: Workload môže byť vysoko frekventovaný (tisíce položiek), ale UI potrebuje len niekoľko aktualizácií za sekundu. V praxi stačí 5–10 aktualizácií/s, pri veľmi rýchlych joboch dokonca 2–4. Všetko nad tým je zvyčajne len optický šum a stojí CPU čas v Message Pump.

Vlákno‑bezpečné počítanie: atomické namiesto zámku

Pre jednoduchý ProgressBar často stačí atomický čítač. „Atomické“ znamená: inkrement a čítanie prebehne bez race condition, typicky cez TInterlocked. Tým sa vyhnete locks (Critical Sections) v hot path slučky.

Osvedčený minimálny prístup:

  • Celkové množstvo je vopred známe (napr. počet záznamov, súborov, ID).
  • Každá iterácia atomicky zvýši DoneCounter.
  • Jeden UI‑Timer periodicky číta počítadlo a nastaví ProgressBar.Position.

Výhoda: Žiadne TThread.Queue na položku, žiadne preťaženie UI. Nevýhoda: Nemáte detailné hlásenia pre každú položku (napr. názov súboru). Namiesto toho môžete doplniť druhé, obmedzené stavové hlásenie (pozri nasledujúcu sekciu).

Stavové hlásenia bez spamu: „letzter Status gewinnt“

Ak chcete navyše zobrazovať krátky text (aktuálna položka, fáza, chybové hlásenie), potrebujete vzor, ktorý pri každom kroku Worker nezahlcuje UI. V praxi veľmi dobre funguje princíp „letzter Status gewinnt“:

  • Worker zapíše statusovú informáciu do vláknovo bezpečnej štruktúry (napr. atomicky vymeniteľný string alebo chránené malým zámkom).
  • UI‑Timer periodicky prevezme naposledy zaznamenaný stav do labelu.

Tým zostane UI responzívne a zároveň vidíte, že „niečo sa deje“. Dôležité je tu menej samotný string než jeho životnosť: neťahajte referencie na krátkoživotné objekty z Worker‑vlákien do UI‑vlákna. Ak odovzdávate objekty, určite vlastníctvo (ownership) explicitne.

TParallel.For vlákno‑bezpečné Progress‑UI s TThread.Queue: robustný vzor

Sú scenáre, kde UI‑Timer sám o sebe nestačí: napr. keď chcete na konci zaručene spustiť presne jedno „Hotovo“ update alebo keď je UI‑aktualizácia zložitejší krok (napr. zápis do logového okna, ale obmedzene). Vtedy je TThread.Queue vhodné – ale nie na každú iteráciu, ale cielene.

Praktický prístup je fronta len pre udalosti s nízkou frekvenciou:

  • Start‑udalosť (príprava UI, zablokovanie tlačidiel)
  • Periodické progress‑udalosti (max. každých X milisekúnd)
  • Chybové udalosti (voliteľne zhromaždené)
  • Done‑udalosť (reset UI, zobraziť výsledok)

Periodicitu nerealizujete cez UI‑vlákno, ale už v kontexte Workerov: necháte Workerov queue‑ovať UI‑update len vtedy, keď od poslednej UI‑aktualizácie uplynul dostatočný čas. Na to sa hodí monotónny zdroj času (napr. TickCount) plus atomická hodnota „last update“.

Dôležité: samotná UI‑aktualizácia musí byť „rýchla“. Nákladné výpočty, súborové I/O alebo databázové prístupy nepatria do gequeueovaného UI‑callbacku. Callback by mal len prečítať stavy a nastaviť ovládacie prvky.

Spracovanie prerušenia: zrušenie bez zaseknutia

V reálnych aplikáciách nie je zrušenie voliteľné. Rozhodujúce je: Cancel nie je „Kill“, ale kooperatívne ukončenie. Workeri musia pravidelne kontrolovať, či je nastavený signál na prerušenie, a potom sa korektne ukončiť. V Delphi na to existuje niekoľko ciest (v závislosti od PPL‑konštrukcie): vlastný Volatile‑flag, atomický Boolean alebo koncept Cancellation cez Tasks (v závislosti od verzie a štruktúry Delphi).

Pre prevádzku sú dôležité dve pravidlá:

  • Cancel muss schnell sichtbar werden: Kontrolujte príznak zrušenia na vhodných miestach, nie len na konci iterácie, keď iterácia môže trvať aj niekoľko sekúnd.
  • Cancel muss aufräumen: Otvorené Handles, temporäre Dateien, Transaktionen oder Locks dürfen nicht liegen bleiben. Das heißt: In jeder Worker-Iteration sind try/finally-Blöcke Pflicht, wenn Ressourcen beteiligt sind.

UI-seitig sollte Cancel nur ein Signal setzen und die UI in einen „Stopping…“-Zustand bringen. Die eigentliche Beendigung und das Reaktivieren der UI passieren dann im Done-Event, nicht sofort beim Klick.

Prevencia deadlockov: die häufigsten Fallen in der Praxis

Diagramm eines zyklischen Wartens zwischen Threads als Visualisierung eines Deadlocks
Deadlocks entstehen oft durch zyklisches Warten: UI-Thread blockiert, Worker warten auf UI-Zugriff.

Pasca 1: WaitFor/Task.Wait im Main Thread

Wenn der Main Thread blockiert, kann er keine Queue-Callbacks ausführen und keine Messages verarbeiten. Das wirkt wie ein Deadlock, auch wenn die Worker korrekt weiterlaufen. Lösung: Keine blockierenden Waits im UI-Thread. Stattdessen Abschlussaktion über TThread.Queue oder eine Ereignissteuerung (z. B. Timer prüft „fertig“).

Pasca 2: Synchronize innerhalb eines Locks

Ein Klassiker: Worker hält eine Critical Section, ruft dann Synchronize, und im UI-Callback wird (direkt oder indirekt) wieder dieselbe Critical Section benötigt. Ergebnis: Kreis warten. Die Regel ist simpel: Keine UI-Übergabe (Synchronize/Queue) aus einem gehaltenen Lock heraus. Wenn ein Lock nötig ist, holen Sie alle Daten erst in lokale Variablen, verlassen den Lock, dann queue’n Sie.

Pasca 3: UI-Callback triggert Reentrancy

Manchmal ist das UI-Update selbst nicht „harmlos“: Setzen von Properties kann Events auslösen (OnChange, OnResize), die wiederum Logik anwerfen, die auf Worker-Zustände zugreift. Das ist kein Deadlock im engeren Sinne, aber führt zu schwer erklärbaren Hängern und Race Conditions. Abhilfe: UI-Updates in „stille“ Pfade legen (Events temporär deaktivieren) oder Reentrancy-Guards nutzen (z. B. atomarer Guard für Update-Phase).

Pasca 4: Zu viele queued Updates

Auch ohne Waits kann die UI „stehen“, wenn Sie zehntausende queued Callbacks erzeugen. Symptome: ProgressBar rennt lange nach, Fenster reagiert träge, CPU im Main Thread hoch. Lösung: Drosseln (Zeitfenster), aggregieren (Counter), oder eine echte Producer/Consumer-Struktur, bei der nur ein UI-Update „pending“ sein kann (Coalescing).

Wenn es komplizierter wird: Ergebnisse sammeln, Fehler bündeln, Reihenfolgen garantieren

TParallel.For ist ideal, wenn Iterationen unabhängig sind. In Business-Software sind Iterationen aber oft nur „weitgehend“ unabhängig: Sie lesen Dateien, rufen REST-APIs, schreiben Datenbankzeilen. Dann müssen Sie drei zusätzliche Punkte sauber planen:

  • Thread-sichere Ergebnis-Sammlung: Entweder pro Thread ein lokaler Buffer (am Ende zusammenführen) oder eine threadsichere Queue/Collection. Locks im Hot Path vermeiden.
  • Spracovanie chýb: Výnimky z worker vlákien musia byť zachytené. V praxi sa osvedčilo: zapamätať si prvú výnimku a vyvolať Cancel, alebo zhromaždiť všetky výnimky a na konci ich zobraziť zhrnuté.
  • Poradie: Ak výstup vyžaduje stabilné poradie (napr. logy podľa indexu), je paralelné spracovanie s následným zotriedením často jednoduchšie než „vláknovo-bezpečné zoradené vkladanie“.

Pre UI to znamená: Nezobrazujte každú chybovú správu okamžite. To vedie k peklu modálnych dialógov. Zhromažďujte chyby (napr. zoznam reťazcov) a na konci zobrazte súhrn alebo log vhodný na export.

Debugging: Ako deadlock skutočne spraviť viditeľným

Deadlocky v paralelnom kóde sú frustrujúce, pretože v debuggeri môžu vyzerať inak než v release. Napriek tomu existuje niekoľko veľmi praktických postupov:

Využitie okna Threads a zásobníkov volaní

Ak sa UI zaseká, pozrite si všetky vlákna: Kde sa nachádza hlavné vlákno? Čaká? Je v smyčke správ? Kde sú worker vlákna? Ak sa workery zasekávajú v Synchronize, príčina je takmer vždy „hlavné vlákno zablokované“ alebo „hlavné vlákno potrebuje zámok“.

Označenie miest Queue-/Synchronize

Cieľavedome umiestnite logovanie na miesta odovzdania (pred Queue, v Queue-callbacku, na konci iterácie). V prevádzke je to často cennejšie než breakpointy, pretože načasovanie je rozhodujúce. Dávajte pozor, aby samotné logovanie bolo vláknovo bezpečné a neblokujúce (napr. žiadne UI-logovanie priamo z worker vlákien).

Meranie času poslednej aktualizácie

Ak sa UI „zaseká“, môže to byť jednoducho tým, že musí spracovať príliš veľa aktualizácií. Preto merajte v hlavnom vlákne, ako často za sekundu vykonávate UI-aktualizáciu a ako dlho trvá. Ak UI-callbacky potrebujú viac než niekoľko milisekúnd, je potrebné obmedzenie alebo zjednodušenie.

Kedy sa TParallel.For s Progress-UI naozaj oplatí?

Paralelizácia nie je cieľ sama o sebe. Oplatí sa najmä, keď:

  • iterácie sú dostatočne dlhé (milisekundy až sekundy), takže režijné náklady threadpoolu sa stratia,
  • úloha je CPU-intenzívna (parsing, kompresia, hashing) alebo má dobre paralelizovateľný I/O (viac súborov, viac HTTP-requestov s limitmi),
  • máte jasnú stratégiu pre Cancel a spracovanie chýb,
  • požiadavky UI sa uspokoja s agregovaným Progressom.

Paralelizácia sa menej oplatí, ak je každá iterácia extrémne krátka (mikrooperácie) alebo ak všetky iterácie narážajú na ten istý úzky hrdlo (seriálna DB-transakcia, globálny zámok, jediný súbor). Vtedy je rýchlejším riešením často: zlepšiť algoritmus, spracovávať v dávkach, zredukovať prístup k dátam alebo úzky hrdlo explicitne oddeliť.

Praktický kontrolný zoznam: Ako udržať UI stabilnú

  • hlavné vlákno nie je zablokované: žiadne blokujúce čakania, žiadne dlhé slučky bez message pump.
  • workery nikdy nesiahajú na Controls: žiadne prístupy VCL/FMX mimo UI vlákna.
  • UI-aktualizácie sú regulované: counter/timer alebo coalesced Queue-aktualizácie namiesto na každú iteráciu.
  • žiadne volania Synchronize zovnútra zámkov.
  • Cancel je kooperatívny, často kontrolovaný a dôsledne upratuje.
  • výnimky sa zhromažďujú a na konci sa spracujú usporiadane.

Záver: Oddeliť cez Queue, stabilizovať pomocou agregácie

Robustné zobrazenie priebehu pri TParallel.For nevznikne len tým, že sa niekde zavolá „Synchronize“, ale jasným architektonickým princípom: pracovné vlákna pracujú nezávisle, Main Thread zostáva voľný a spracúva len niekoľko rýchlych UI-aktualizácií. TThread.Queue je pritom správny nástroj, ak ho používate cielene a regulovane. Za „pomalé“ alebo nestabilné správanie sú takmer vždy zodpovedné dve príčiny: Main Thread niekde čaká blokujúcim spôsobom – alebo sa topí v príliš veľkom množstve zaradených aktualizácií.

Keď vzor raz správne nastavíte (Counter/Coalescing, Cancel-Flag, Abschluss-Callback), oplatí sa ho použiť v mnohých častiach existujúcej Delphi aplikácie: Import/Export, kontrola dát, súborové a API úlohy – všetko je potom responzívnejšie, bez toho aby ste si pri každom Progress-Update privodili nové deadlocky.

Ak potrebujete podporu pri stabilizácii paralelného kódu, pri debugovaní zamŕzania UI alebo pri čistom modernizovaní existujúcich Delphi aplikácií: Kontaktujte nás.

Pre túto tému sú dôležité aj Delphi Parallel Programming Library a Tthread.queue Vs Synchronize. Príspevok tieto aspekty zrozumiteľne zaradí do kontextu a ukáže, na čo to v bežnej praxi záleží.

Prediskutovať projekt alebo modernizačný zámer s Net-Base.

ďalší krok

Keď sa z témy stane reálny projekt, architektúru, existujúci stav a prevádzku treba včas posudzovať spoločne.

Podporujeme nielen pri jednotlivých otázkach, ale aj vtedy, keď sa z fragmentov zdrojového kódu, tém súvisiacich s legacy systémami alebo nápadov na portál má stať robustný podnikový projekt.

  • Stav, cieľový obraz a technické riziká sa hodnotia spoločne.
  • REST, prístup k údajom, portály a nasadenie nebudú odložené na neskôr ako následné úlohy.
  • Včas identifikujete, ktorá cesta je ekonomicky a prevádzkovo životaschopná.

Zdieľať príspevok

Tento príspevok priamo zdieľať

LinkedIn, X, XING, Facebook, WhatsApp a e‑mail sú okamžite k dispozícii. Pre Instagram pripravíme priamo odkaz a stručný text.

E-mail

Instagram sa otvorí v novej karte. Odkaz a krátky text sa predtým skopírujú do schránky.