Net-Base Magazin

24.07.2026

Delphi: TParallel.For szálbiztos Progress-UI-val (TThread.Queue) holtpontok nélkül

Így kombinálja a Delphi TParallel.For-t egy szálbiztos Progress-UI-val: frissítések TThread.Queue-n keresztül, tiszta aggregálás, megszakítás-kezelés és tipikus holtpont-csapdák hibakeresésben és üzemeltetésben.

24.07.2026

A magazintémától a projektgyakorlatig

A bejegyzéshez tartozó szolgáltatási és technikai oldalak

Akinek a Delphi-ben számításigényes vagy I/O-terhelt feladatokat kell párhuzamosítania, gyorsan a Parallel Programming Library (PPL) és konkrétan a TParallel.For mellett találja magát. Az eredmény gyakran azonnal mérhető – egészen addig, amíg ugyanabban a lélegzetben egy Progress-UI-t „csak gyorsan” frissíteni szeretnénk. Pont itt keletkeznek a tipikus akadozások: látszólag véletlenszerű UI-fagyások, egy ProgressBar, amely visszalép, vagy komplett deadlock, amint a debuggerben lépésenként haladunk.

Ez a bejegyzés a TParallel.For threadsichere Progress-UI-ról szól: egy robusztus minta, amely VCL és FMX alatt működik, a UI-frissítéseket TThread.Queue segítségével tisztán egyesíti, figyelembe veszi a Cancel/Abort eseteket, és következetesen elkerüli a leggyakoribb deadlock-csapdákat. A fókusz az üzemi valóságon van: reprodukálható viselkedés, érthető felelősségek és hibakeresési tanácsok, amelyek akkor is segítenek, ha a hiba csak „ügyfélnél” jelentkezik.

Miért siklik el olyan gyakran a Progress-UI TParallel.For esetén

TParallel.For tipikusan a Delphi-threadpoolból származó worker-threadeken fut. Ezek a threadek nem nyúlhatnak közvetlenül VCL- vagy FMX-vezérlőkhöz, mert a UI-keretrendszerek (Message Loop, Window Handles, Rendering) a Main Thread-hez kötöttek. Már egy látszólag ártalmatlan ProgressBar.Position := … egy workerből nem meghatározott viselkedéshez vezethet: sporadikus AVs, lefagyott ablakok vagy „villódzó” frissítések.

A kézenfekvő javítás gyakran a TThread.Synchronize. Ez bár megoldja a thread-biztonságot, párhuzamos ciklusokban gyorsan egy másik problémát idéz elő: soros szűk keresztmetszetet hoz létre. Minden worker a UI-threadre vár, amely viszont a renderelésre és a Synchronize-hívások feldolgozására van lekötve. Terhelés alatt ez deadlocknak tűnhet – még ha valójában csak egy starvation/lockstep effektus is.

Van aztán a valódi deadlock-osztály: a Main Thread (például WaitFor, Task.Wait vagy indirekt, blokkoló hívások révén) a párhuzamos művelet befejezésére vár, miközben a worker-threadek Synchronize vagy nem szerencsés Queue-használat révén próbálnak munkát bedobni a Main Thread-be. Eredmény: a Main Thread a workerre vár, a worker pedig a Main Thread-re.

TThread.Queue vs. TThread.Synchronize: a gyakorlati különbség

Elmosódott debugger-nézet jegyzetekkel a threadekről és a Queue-ról a Queue vs Synchronize kontextusában
Hibakeresésnél gyorsan kiderül, hogy a worker-threadek valóban a Main Thread-re várnak-e, vagy csak queued frissítéseket küldenek.

Mindkét mechanizmus arra szolgál, hogy a kód biztonságosan a Main Thread-en fusson. A különbség a várakozási szemantikában rejlik:

  • TThread.Synchronize: a hívó worker vár, amíg a Main Thread végrehajtja a kódot. Ez „szinkron”, növeli a késleltetést, és klasszikus összetevője a deadlockoknak, ha a Main Thread épp blokkolva van.
  • TThread.Queue: A Worker a kódot csak a Main Thread számára egy sorba helyezi, majd folytatja a futást. Ez „asynchron”, leválasztja a szálakat, és párhuzamos forgatókönyvekben szinte mindig jobb alapértelmezett választás – feltéve, hogy kontrolláljuk a frissítések gyakoriságát.

Fontos: Queue nem ad felmentést. Ha egy ciklus minden iterációjában küld egy Queue-frissítést, túlterheli a Main-Thread-Queue-t. Ilyenkor a UI nem deadlock miatt akad meg, hanem a parancsok puszta mennyisége miatt. A felület „záslatosnak”/„lassúnak” tűnik, és a feldolgozás vége késik, mert még százak vagy ezrek UI-frissítések futnak le késleltetve.

A fájdalmas szélső eset: várakozás az UI-szálon

Vállalati alkalmazásokban gyakran a következő folyamatot látjuk: rákattintanak a „Start” gombra, az UI deaktiválódik, megjelenik egy ProgressDialog, majd szinkron módon „várakoznak”, amíg minden kész nem lesz, és csak ezután aktiválják újra. Ez a minta sok deadlock magja.

Típusos variánsok (kódbázistól függően):

  • A Main Thread elindít egy TParallel.For-t, majd egy blokkoló várakozó logikát hív meg utána (közvetlenül vagy közvetve).
  • Egy ProgressDialog a Constructor-ban vagy az OnShow-ban meghív egy rutint, amely belső várakozást végez.
  • Egy Mégse-gomb ugyan beállít egy flag-et, de a Main Thread mégis egy várakozó ciklusban ragad.

Ha a Worker-Threads ezalatt Synchronize-t használnak, a deadlock gyakorlatilag garantált. A Queue is okozhat akadozást, ha a Main Thread blokkolt és nem pumpál üzeneteket – ilyenkor a Queue sem kerül feldolgozásra.

A gyakorlati következtetés: A Main Thread nem várakozhat blokkoló módon a párhuzamos ciklusra, ha párhuzamos UI-frissítésekre van szükség. Ehelyett a feldolgozást vagy teljesen át kell helyezni egy háttérfeladatra, vagy szervezni kell egy „aszinkron befejezést” (callback/Queued befejező művelet), amely a végén visszaadja az UI irányítását.

Tisztább megközelítés: a Progress csak aggregált, az UI-frissítések korlátozottak

Munkaállomás-motiv absztrahált Progress-kijelzővel és időméréssel, a korlátozott UI-frissítések jelképeként
Az aggregálás és időalapú korlátozás megakadályozza, hogy az UI-t túl sok frissítés elárassza.

Egy robusztus minta három, világosan elkülönített felelősségre épül:

  • Worker-Threads végzik az egyes elemen/indexen a tényleges munkát. Csak threadsafe módon jelentik a haladást (számláló, Queue, thread-safe Queue).
  • Aggregator (gyakran: a Main Thread vagy egy dedikált timer az UI-ban) kiszámítja a haladásból az UI-státuszt (pozíció, szöveg, ETA) és frissíti a vezérlőket. Így elkerülhető az 1:1 frissítés iterációnként.
  • Befejezés (szintén a Main Thread-en): az UI újraaktiválása, az eredmény megjelenítése, a hibák összefoglalása és az erőforrások felszabadítása.

Miért működik ez a szétválasztás jól: a munkaterhelés magas frekvenciájú lehet (több ezer elem), de a felhasználói felületnek csak néhány frissítésre van szüksége másodpercenként. A gyakorlatban 5–10 frissítés/mp elegendő, nagyon gyors feladatoknál akár 2–4 is. Minden e fölött általában csak vizuális zaj, és CPU-időt emészt fel a Message Pump-ban.

Szálbiztos számlálás: atomikus műveletek a zárolás helyett

Egy egyszerű ProgressBar-hoz gyakran elegendő egy atomikus számláló. „Atomikus” azt jelenti: az inkrement és az olvasás Race Condition nélkül történik, tipikusan TInterlocked használatával. Így elkerülheti a lockokat (Critical Sections) a ciklus hot path-jén.

Bevált minimálmegoldás:

  • A teljes mennyiség előre ismert (pl. rekordok száma, fájlok, azonosítók).
  • Minden iteráció atomikusan növeli a DoneCounter-t.
  • Egy UI-timer periódikusan olvassa a számlálót és beállítja a ProgressBar.Position-t.

Előny: nincs TThread.Queue elemenként, nincs UI-túlterhelés. Hátrány: nincs részletes értesítés elemenként (pl. fájlnév). Ezt pótolhatja egy második, ritkított státuszüzenettel (lásd következő szakasz).

Státuszüzenetek spam nélkül: „az utolsó státusz érvényesül”

Ha emellett rövid szöveget is meg akar jeleníteni (aktuális elem, fázis, hibaüzenet), szintén szükség van egy mintára, amely nem árasztja el a UI-t minden worker-lépésnél. A gyakorlatban a „az utolsó státusz érvényesül” megközelítés jól működik:

  • A worker egy státuszinformációt ír egy szálbiztos struktúrába (pl. atomikusan cserélhető string, vagy kis zárral védett).
  • Egy UI-timer periódikusan átveszi az utoljára látott státuszt egy Label-be.

Így a UI reagálóképessége megmarad, és mégis látható, hogy „valami történik”. Itt kevésbé maga a string a fontos, mint az élettartam: ne vigyen át hivatkozásokat rövid életű objektumokra a worker-szálakról a UI-szálra. Ha objektumokat ad át, tisztázza a tulajdonjogot egyértelműen.

TParallel.For szálbiztos Progress-UI TThread.Queue-val: egy robusztus minta

Vannak olyan forgatókönyvek, amikor egy UI-timer önmagában nem elegendő: például ha a végén biztosan pontosan egy „Fertig”-frissítést szeretne kiváltani, vagy ha az UI-frissítés összetettebb lépés (pl. bejegyzés egy log-ablakba, de ritkítva). Ilyenkor a TThread.Queue megfelelő – de nem iterációnként, hanem célzottan.

Egy gyakorlatias megközelítés egy csak alacsony frekvenciájú eseményekre szánt Queue:

  • Start-Event (UI előkészítése, gombok letiltása)
  • Periodikus Progress-Events (max. minden X milliszekundumban)
  • Fehler-Events (opcionálisan gyűjtve)
  • Done-Event (UI visszaállítása, eredmény megjelenítése)

A periodikusságot nem a UI-szálon, hanem már a worker-környezetben éri el: úgy engedi a workert, hogy csak akkor queue-olja az UI-frissítést, ha az utolsó UI-frissítés óta elegendő idő eltelt. Ehhez alkalmas egy monotón időforrás (pl. TickCount) plusz egy atomikus „last update”-érték.

Fontos: maga az UI-frissítés gyorsnak kell lennie. Drága számítások, fájl-I/O vagy adatbázis-hozzáférések nem tartoznak a queu-olt UI-visszahívásba. A visszahívásnak csak állapotokat kell kiolvasnia és vezérlőket beállítania.

Cancel-Handling: megszakítás lefagyás nélkül

Valódi alkalmazásokban a megszakítás nem opcionális. Fontos: a Cancel nem „Kill”, hanem egy kooperatív leállítás. A workereknek rendszeresen ellenőrizniük kell, hogy be van-e állítva egy megszakítási jel, és akkor tisztán ki kell lépniük. A Delphi-ben több mód is van erre (a PPL-konstrukciótól függően): egy saját Volatile-flag, egy atomikus Boolean, vagy egy Cancellation-koncepció Tasks-on keresztül (a Delphi verziójától és struktúrájától függően).

A működtetés szempontjából két szabály fontos:

  • A Mégse gombnak gyorsan láthatónak kell lennie: Ellenőrizze a megszakítási jelzőt ésszerű pontokon, ne csak az iteráció végén, ha maga az iteráció akár másodpercekig is tarthat.
  • A Mégse-nek takarítania kell: Nyitott handle-ek, ideiglenes fájlok, tranzakciók vagy zárolások nem maradhatnak hátra. Tehát: minden Worker-iterációban kötelező a try/finally-blokk, ha erőforrások érintettek.

UI-oldalról a Mégse csak jelet adjon, és a felhasználói felületet egy „Leállítás…“ állapotba tegye. A tényleges befejezés és a UI újraaktiválása a Done-Event-ben történjen, ne rögtön a kattintáskor.

Holtpontok elkerülése: a gyakorlat leggyakoribb csapdái

Diagram a szálak közötti ciklikus várakozásról, a holtpont vizualizációja
A holtpontok gyakran ciklikus várakozás miatt alakulnak ki: a UI-szál blokkolódik, a Worker-ek a UI-hozzáférésre várnak.

Csapda 1: WaitFor/Task.Wait a fő szálon

Ha a fő szál blokkolódik, nem tud Queue-callbackokat végrehajtani és nem tud üzeneteket feldolgozni. Ez holtpontnak tűnik, még akkor is, ha a Worker-ek rendben futnak tovább. Megoldás: Ne használjon blokkoló Wait-eket a UI-szálon. Ehelyett a befejező műveletet végezze TThread.Queue-vel vagy eseményvezérléssel (pl. egy timer ellenőrzi, hogy „kész“).

Csapda 2: Synchronize egy zárolás (Lock) belsejében

Egy klasszikus eset: a Worker tart egy kritikus szakaszt, majd meghívja a Synchronize-ot, és az UI-callbackban (közvetlenül vagy közvetve) ismét ugyanarra a kritikus szakaszra van szükség. Eredmény: körkörös várakozás. A szabály egyszerű: Ne adjon át UI-műveletet (Synchronize/Queue) egy megtartott lockból. Ha zárolás szükséges, olvassa ki az összes adatot lokális változókba, lépjen ki a lockból, majd küldje a queue-ba.

Csapda 3: Az UI-callback újrabelépést (Reentrancy) vált ki

Az UI-frissítés maga sem mindig „ártalmatlan“: Property-k beállítása eseményeket válthat ki (OnChange, OnResize), amelyek viszont logikát indítanak, ami a Worker-állapotokra támaszkodik. Ez nem holtpont szigorúan véve, de nehezen magyarázható fagyásokhoz és versenyhelyzetekhez vezet. Megoldás: helyezze az UI-frissítéseket „csendes“ utakra (események ideiglenes letiltása), vagy használjon újrabelépés elleni őröket (pl. atomikus őr az update-fázishoz).

Csapda 4: Túl sok sorba állított (queued) frissítés

Még Wait-ek nélkül is megállhat a UI, ha tízezrével hoz létre queued callbackeket. Tünetek: a ProgressBar hosszasan utólag fut, az ablak lassan reagál, a fő szál CPU-terhelése magas. Megoldás: csillapítás (időablak), aggregálás (számláló), vagy valódi termelő/fogyasztó-struktúra, ahol csak egy UI-frissítés lehet „pending“ (összevonás).

Ha bonyolultabbá válik: eredmények gyűjtése, hibák összegzése, sorrendek garantálása

TParallel.For ideális, ha az iterációk függetlenek. Üzleti szoftverben az iterációk azonban gyakran csak „nagyjából“ függetlenek: fájlokat olvasnak, hívnak REST-APIs-t, adatbázis-sorokat írnak. Ilyenkor három további pontot kell alaposan megtervezni:

  • Szálbiztos eredménygyűjtés: Vagy thread-onként egy lokális puffer (a végén egyesítve), vagy egy szálbiztos Queue/Collection. Kerülje a zárolásokat a hot pathon.
  • Hibakezelés: A worker-szálakból származó kivételeket össze kell gyűjteni. A gyakorlatban beválik: az első kivétel megjegyzése és a Cancel kiváltása, vagy minden kivételt összegyűjteni és a végén egyben megjeleníteni.
  • Sorrend: Ha a kimenet stabil sorrendet igényel (pl. logok index szerinti sorrendje), a párhuzamos feldolgozás utólagos rendezéssel gyakran egyszerűbb, mint a „szálbiztos rendezett beszúrás”.

A UI számára ez azt jelenti: Ne jelenítsen meg minden egyes hibaüzenetet azonnal. Ez modal párbeszédablak-pokolhoz vezet. Gyűjtse össze a hibákat (pl. stringek listája), és a végén mutasson összefoglalót vagy egy exportálható naplót.

Debugging: Hogyan tegye a holtpontot valóban láthatóvá

A párhuzamos kódban előforduló holtpontok frusztrálóak, mert a hibakeresőben máshogy nézhetnek ki, mint a release buildben. Mégis vannak néhány nagyon praktikus eszköz:

Szálak-ablak és hívásveremek használata

Ha a UI lefagy, nézze meg az összes szálat: Hol áll a főszál? Várakozik? Üzenetciklusban van? Hol tartózkodnak a worker-szálak? Ha a worker-ek a Synchronize-ben akadoznak, az oka szinte mindig az, hogy a főszál blokkolt vagy a főszálnak zárolásra van szüksége.

Queue-/Synchronize-helyek megjelölése

Helyezzen célzott naplózást az átadási pontokra (a Queue előtt, a Queue-callbackben, az iteráció végén). Üzem közben ez gyakran többet ér, mint töréspontok, mert az időzítés döntő. Ügyeljen arra, hogy a naplózás maga is szálbiztos és nem blokkoló legyen (pl. ne jelenítsen meg UI-naplózást közvetlenül worker-szálakból).

Utolsó frissítés idejének mérése

Ha a UI „lefagy”, egyszerűen előfordulhat, hogy túl sok frissítést kell feldolgoznia. Mérje ezért a főszálon, hogy másodpercenként hányszor hajt végre UI-frissítést és mennyi ideig tart. Amint az UI-callbackek több mint néhány milliszekundumot igényelnek, szükséges a visszafogás vagy egyszerűsítés.

Mikor éri meg valóban a TParallel.For Progress-UI-val?

A párhuzamosítás nem öncélú. Különösen megéri, ha:

  • az iterációk elég nagyok (milliszekundumoktól másodpercekig), így a threadpool-overhead elenyészik,
  • a feladat CPU-intenzív (parszolás, tömörítés, hashing) vagy jól párhuzamosítható I/O-val rendelkezik (több fájl, több HTTP-kérés limitálással),
  • van egy egyértelmű Cancel- és hibastratégia,
  • a UI-követelmények beérik aggregált előrehaladással.

Kevésbé éri meg, ha minden iteráció rendkívül rövid (mikroműveletek), vagy ha az összes iteráció ugyanarra a szűk keresztmetszetre fut (soros DB-tranzakció, globális zárolás, egyetlen fájl). Ilyenkor a gyorsabb megoldás gyakran: algoritmus javítása, batchelés, adathozzáférés csökkentése vagy a szűk keresztmetszet explicit elkülönítése.

Gyakorlati ellenőrzőlista: így marad stabil a UI

  • A főszál ne legyen blokkolva: ne legyenek várakozások, ne legyenek hosszú ciklusok üzenetpumpa nélkül.
  • A worker-ek soha ne érintsenek Controls-okat: ne legyen VCL/FMX-hozzáférés a UI-szálon kívül.
  • A UI-frissítések korlátozottak: számláló/timer vagy összevont Queue-frissítések per iteráció helyett.
  • Ne legyenek Synchronize-hívások lockok belsejéből.
  • A Cancel kooperatív, gyakran ellenőrzött és gondosan takarít utána.
  • A kivételeket összegyűjtik és a végén rendezett módon kezelik.

Fazit: Queue-val elkülöníteni, aggregációval stabilizálni

Egy robusztus Progress-UI a TParallel.For esetén nem attól jön létre, hogy „valahova Synchronize-t beleteszünk”, hanem egy világos architekturális elv: a worker-szálak függetlenül dolgoznak, a főszál szabad marad és csak néhány, gyors UI-frissítést dolgoz fel. TThread.Queue ebben az esetben a megfelelő eszköz, ha célzottan és szabályozottan használja. „Lassú” vagy instabil viselkedésnél szinte mindig két ok felelős: a főszál valahol blokkolva vár – vagy elsüllyed túl sok sorba állított frissítésben.

Ha ezt a mintát egyszer rendesen felépítette (Counter/Coalescing, Cancel-Flag, Abschluss-Callback), megéri sok helyen egy meglévő Delphi-alkalmazásban alkalmazni: Import/Export, adatellenőrzések, fájl- és API-feladatok – minden reaktívabbá válik anélkül, hogy minden egyes Progress-Update-nél új deadlockokat szerezne.

Ha támogatásra van szüksége a párhuzamos kód stabilizálásához, a UI-akadások hibakereséséhez vagy a meglévő Delphi-alkalmazások tiszta modernizálásához: vegye fel a kapcsolatot.

Ehhez a témához kapcsolódóan fontosak a Delphi Parallel Programming Library és a Tthread.queue Vs Synchronize. A bejegyzés érthetően rendezi ezeket a szempontokat és bemutatja, mire kell figyelni a gyakorlatban.

Projekt vagy modernizációs terv megvitatása Net-Base kapcsán.

Következő lépés

Ha a téma valós projektté válik, az architektúrát, a meglévő rendszert és az üzemeltetést már korán együtt kell értékelni.

Nemcsak egyedi kérdésekben támogatunk, hanem akkor is, amikor forráskódrészletekből, örökölt rendszerekkel kapcsolatos témákból vagy portálötletekből robusztus vállalati projektet kell kialakítani.

  • A jelenlegi állapotot, a célállapotot és a műszaki kockázatokat együttesen értékeljük.
  • REST, az adathozzáférés, a portálok és a Rollout nem kerülnek utólagos teendőkként elhalasztásra.
  • Már korán láthatja, melyik út gazdaságilag és üzemeltetési szempontból életképes.

Bejegyzés megosztása

Ezt a bejegyzést közvetlenül megosztani

LinkedIn, X, XING, Facebook, WhatsApp és e-mail azonnal elérhetők. Instagramhoz linket és rövid szöveget közvetlenül előkészítünk.

E-mail

Az Instagram egy új lapon nyílik meg. A link és a rövid szöveg előzetesen a vágólapra másolódik.