Van magazinethema naar projectpraktijk
Relevante dienst- en technische pagina's bij het artikel
Wie in Delphi rekenintensieve of I/O-belaste jobs wil paralleliseren, komt snel uit bij de Parallel Programming Library (PPL) en concreet bij TParallel.For. Het effect is vaak direct meetbaar – totdat je in hetzelfde adem de Progress-UI „even“ wilt bijwerken. Juist daar ontstaan de typische haperingen: ogenschijnlijk willekeurige UI-freezes, een ProgressBar die achteruit springt, of een complete deadlock zodra je stap-voor-stap in de debugger loopt.
In dit artikel gaat het over TParallel.For threadsichere Progress-UI: een robuust patroon dat werkt in VCL en FMX, UI-updates netjes bundelt via TThread.Queue, Cancel/Abort meeneemt en de meest voorkomende deadlock-valkuilen consequent omzeilt. De focus ligt op bedrijfspraktijk: reproduceerbaar gedrag, begrijpelijke verantwoordelijkheden en debugging-tips die ook helpen als de fout alleen „bij klanten“ optreedt.
Waarom Progress-UI bij TParallel.For zo vaak misgaat
TParallel.For draait doorgaans op worker-threads uit het Delphi-threadpool. Deze threads mogen geen VCL- of FMX-controls direct aanraken, omdat de UI-frameworks (Message Loop, Window Handles, Rendering) gebonden zijn aan de Main Thread. Zelfs een ogenschijnlijk onschuldige ProgressBar.Position := … vanuit een worker kan leiden tot ongedefinieerd gedrag: sporadische AVs, bevroren vensters of „flikkerende“ updates.
De voor de hand liggende reparatie is vaak TThread.Synchronize. Dat verhelpt weliswaar de thread-safety, maar leidt in parallelle lussen snel tot een ander probleem: het creëert een seriële bottleneck. Elke worker wacht op de UI-thread, die op haar beurt bezig is met renderen en het afhandelen van Synchronize-aanroepen. Onder load lijkt dat op een deadlock – ook al is het „slechts“ een starvation/lockstep-effect.
En dan is er de echte deadlock-klasse: de Main Thread wacht (bijv. via WaitFor, Task.Wait of indirect door blokkerende aanroepen) op het einde van de parallelle operatie, terwijl worker-threads proberen via Synchronize of ongelukkig gebruik van Queue werk naar de Main Thread te sturen. Resultaat: Main Thread wacht op Worker, Worker wachten op Main Thread.
TThread.Queue vs. TThread.Synchronize: het praktische verschil
Beide mechanismen dienen om code veilig in de Main Thread uit te voeren. Het verschil zit in de wachtemantiek:
- TThread.Synchronize: De aanroepende worker wacht tot de Main Thread de code heeft uitgevoerd. Dat is „synchron“, verhoogt latenties en is een klassieke oorzaak van deadlocks als de Main Thread op dat moment geblokkeerd is.
- TThread.Queue: De worker plaatst de code alleen in een wachtrij voor den Main Thread en loopt door. Dat is „asynchroon“, ontkoppelt threads en is in parallelle scenario’s bijna altijd de betere default-keuze – mits men de updatefrequentie controleert.
Belangrijk: Queue is geen vrijbrief. Als u in elke iteratie van een lus een Queue-update verstuurt, overlaadt u de Main-Thread-Queue. De UI hangt dan niet door een deadlock, maar door de pure hoeveelheid berichten. De UI voelt “traag” aan en het einde van de verwerking wordt vertraagd omdat nog honderden of duizenden UI-updates moeten worden afgehandeld.
Het randgeval dat echt pijn doet: wachten in de UI-thread
In bedrijfsapplicaties ziet men vaak de volgende volgorde: knop „Start“ wordt geklikt, UI wordt gedeactiveerd, ProgressDialog verschijnt, vervolgens wordt synchron „gewacht“ tot alles klaar is om daarna weer te activeren. Dit patroon is de kern van veel deadlocks.
Typische varianten (afhankelijk van de codebasis):
- Main Thread start TParallel.For en roept daarna een blokkerende wachtlogica aan (direct of indirect).
- Een ProgressDialog roept in de Constructor of OnShow een routine aan die intern wacht.
- Een Annuleerknop zet weliswaar een flag, maar de Main Thread blijft toch in een wachlus hangen.
Als worker-threads in deze tijd Synchronize gebruiken, is de deadlock praktisch gegarandeerd. Met Queue kan het ook vastlopen als de Main Thread geblokkeerd is en geen messages pusht – want dan wordt ook de Queue niet verwerkt.
De operationeel robuuste consequentie is: de Main Thread mag niet blokkerend wachten op de parallelle lus als er parallel UI-updates nodig zijn. In plaats daarvan moet de verwerking ofwel volledig naar een achtergrondtask worden verplaatst, of men organiseert een „asynchroon einde“ (Callback/Queued afsluitactie) die de UI aan het einde weer vrijgeeft.
Nettere aanpak: voortgang alleen geaggregeerd, UI-updates gedoseerd
Een robuust patroon bestaat uit drie duidelijk gescheiden verantwoordelijkheden:
- Worker-Threads voeren het eigenlijke werk per element/index uit. Ze melden alleen voortgang in een thread-veilige vorm (teller, Queue, thread-safe Queue).
- Aggregator (vaak: Main Thread of een dedicated timer in de UI) berekent op basis van de voortgang een UI-status (positie, tekst, ETA) en werkt controls bij. Zo voorkomt u 1:1-updates per iteratie.
- Afsluiting (eveneens in de Main Thread): UI reactiveren, resultaat tonen, fouten samenvatten, resources vrijgeven.
Waarom deze scheiding zo goed werkt: de Workload kan hoogfrequent zijn (duizenden elementen), maar de UI heeft slechts enkele updates per seconde nodig. In de praktijk volstaan 5–10 updates/s, bij zeer snelle jobs zelfs 2–4. Alles daarboven is meestal slechts visuele ruis en kost CPU-tijd in de Message Pump.
Threadveilig tellen: Atomair statt Lock
Voor een eenvoudige ProgressBar volstaat vaak een atomare teller. „Atomair“ betekent: Inkrement en Read gebeuren zonder Race Condition, typisch via TInterlocked. Daarmee voorkomt u Locks (Critical Sections) in het Hot Path van de lus.
Beproefde minimale aanpak:
- Totale hoeveelheid is van tevoren bekend (bijv. aantal Datensätze, bestanden, IDs).
- Elke iteratie verhoogt atomair een DoneCounter.
- Een UI-Timer leest periodiek de counter en stelt ProgressBar.Position in.
Voordeel: geen TThread.Queue per element, geen UI-overbelasting. Nadeel: u heeft geen detailmeldingen per element (bijv. bestandsnaam). Hiervoor kunt u een tweede, gedoseerde statusmelding toevoegen (zie volgende sectie).
Statusmeldingen zonder Spam: „laatste status wint“
Als u daarnaast een korte tekst wilt tonen (actueel element, fase, foutmelding), is er ook een patroon nodig dat niet bij elke Worker-stap de UI overspoelt. In de praktijk werkt „laatste status wint“ heel goed:
- De Worker schrijft een statusinformatie naar een threadveilige structuur (bijv. atomair uitwisselbare string, of beschermd met een kleine Lock).
- Een UI-Timer neemt periodiek de laatst geziene status over in een label.
Zo blijft de UI responsief, en ziet u toch dat „er iets gebeurt“. Belangrijk is hier minder de string zelf dan de levensduur: geen referenties van kortlevende objecten uit Worker-Threads naar de UI-Thread meezeulen. Als u objecten doorgeeft, verduidelijk dan expliciet de Ownership.
TParallel.For threadsichere Progress-UI mit TThread.Queue: ein robustes Muster
Er zijn scenario’s waarin een UI-Timer alleen niet volstaat: bijv. wanneer u aan het einde zeker precies één „Fertig“-update wilt triggeren, of wanneer de UI-update een complexere stap is (bijv. een invoer in een logvenster, maar gedoseerd). Dan is TThread.Queue geschikt – maar niet per iteratie, maar doelgericht.
Een praktisch bruikbare aanpak is een Queue alleen voor gebeurtenissen met lage frequentie:
- Start-Event (UI voorbereiden, knoppen blokkeren)
- Periodieke Progress-Events (max. elke X Millisekunden)
- Fehler-Events (optioneel verzameld)
- Done-Event (UI resetten, resultaat tonen)
De periodiciteit regelt u niet via de UI-Thread, maar al in de Worker-context: u laat Workers alleen dan een UI-update queuen als er sinds de laatste UI-update voldoende tijd verstreken is. Daarvoor is een monotone tijdbron geschikt (bijv. TickCount) plus een atomaire „last update“-waarde.
Belangrijk: de UI-update zelf moet „snel“ zijn. Zware berekeningen, Datei-I/O of database-toegang horen niet in de via queue geplaatste UI-callback. De callback moet alleen toestanden uitlezen en Controls zetten.
Cancel-Handling: Afbrechen ohne Hänger
In echte toepassingen is annuleren niet optioneel. Cruciaal is: Cancel is geen „Kill“, maar een coöperatieve beëindiging. Worker moeten regelmatig controleren of een Abbruchsignal gezet is, en dan netjes uitstappen. In Delphi zijn daar meerdere wegen voor (afhankelijk van PPL-Konstruktion): een eigen Volatile-Flag, een atomair Boolean, of een Cancellation-concept via Tasks (afhankelijk van Delphi-versie en structuur).
Voor de exploitatie zijn twee regels belangrijk:
- Cancel muss schnell sichtbar werden: Controleer het afbreekflag op zinvolle punten, niet alleen aan het einde van een iteratie als die iteratie soms seconden kan duren.
- Cancel muss aufräumen: Open handles, tijdelijke bestanden, transacties of locks mogen niet achterblijven. Dat betekent: in elke worker-iteratie zijn try/finally-blokken verplicht als resources betrokken zijn.
UI-zijde 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.
Deadlocks vermeiden: die häufigsten Fallen in der Praxis
Falle 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“).
Falle 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.
Falle 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).
Falle 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.
- Foutafhandeling: Exceptions uit Worker-Threads moeten worden verzameld. In de praktijk werkt het goed om: de eerste Exception te onthouden en Cancel uit te voeren, of alle Exceptions te verzamelen en aan het einde samengevat weer te geven.
- Volgorde: Als de uitvoer een stabiele volgorde nodig heeft (bijv. logs op index), is parallelle verwerking met later sorteren vaak eenvoudiger dan „thread-veilig geordend invoegen“.
Voor de UI betekent dit: Toon niet elke afzonderlijke foutmelding direct. Dat leidt tot modale dialoog-hel. Verzamel fouten (bijv. een lijst met strings) en toon aan het einde een samenvatting of een exporteerbaar log.
Debugging: Hoe u de deadlock echt zichtbaar maakt
Deadlocks in parallelle code zijn frustrerend omdat ze in de debugger anders kunnen lijken dan in de release-build. Toch zijn er een paar zeer praktische hefbomen:
Threads-venster en Call Stacks gebruiken
Als de UI vastloopt, bekijk dan alle threads: waar staat de Main Thread? Wacht hij? Zit hij in een Message-Loop? Waar staan de Worker-Threads? Als Worker in Synchronize vastzitten, is de oorzaak bijna altijd „Main Thread geblokkeerd“ of „Main Thread heeft een lock nodig“.
Queue-/Synchronize-locaties markeren
Plaats gerichte logging bij overdrachtslocaties (voor de Queue, in de Queue-callback, aan het einde van de iteratie). In productie is dat vaak waardevoller dan breakpoints, omdat timing bepalend is. Let erop dat logging zelf thread-veilig en niet-blockerend is (bijv. geen UI-loguitvoer direct vanuit Worker-Threads).
Laatste update-tijd meten
Als de UI „vastloopt“, kan het simpelweg zijn dat deze te veel updates moet verwerken. Meet daarom in de Main Thread hoe vaak per seconde u een UI-update uitvoert en hoe lang dat duurt. Zodra UI-callbacks meer dan een paar milliseconden nodig hebben, is beperking of vereenvoudiging nodig.
Wanneer loont TParallel.For met progress-UI zich echt?
Parallelisatie is geen doel op zichzelf. Het loont vooral wanneer:
- de iteraties groot genoeg zijn (milliseconden tot seconden), zodat Threadpool-overhead verwaarloosbaar is,
- de taak CPU-intensief is (parsing, compressie, hashing) of goed paralleliseerbare I/O heeft (meerdere bestanden, meerdere HTTP-requests met limieten),
- u een duidelijke Cancel- en foutenstrategie heeft,
- de UI-eisen volstaan met geaggregeerde progress-weergave.
Het loont minder wanneer elke iteratie extreem kort is (micro-operaties) of wanneer alle iteraties op dezelfde bottleneck lopen (een seriële DB-transactie, een globale lock, één enkel bestand). Dan is de snellere hefboom vaak: algoritme verbeteren, batchen, datatoegang verminderen of de bottleneck expliciet ontkoppelen.
Praktijk-checklist: zo blijft de UI stabiel
- Main Thread niet geblokkeerd: geen waits, geen lange loops zonder message pump.
- Workers raken nooit Controls aan: geen VCL/FMX-toegang buiten de UI-thread.
- UI-updates zijn gedempt: counters/timers of geaggregeerde Queue-updates in plaats van per iteratie.
- Geen Synchronize-aanroepen vanuit locks.
- Cancel is coöperatief, wordt vaak gecontroleerd en ruimt netjes op.
- Exceptions worden verzameld en aan het einde geordend afgehandeld.
Conclusie: Ontkoppel met Queue, stabiliseer met aggregatie
Een robuuste Progress-UI bij TParallel.For ontstaat niet door „ergens Synchronize in te gooien“, maar door een helder architectuurprincipe: workers werken onafhankelijk, de Main Thread blijft vrij en verwerkt alleen enkele, snelle UI-updates. TThread.Queue is daarbij het juiste gereedschap als u het doelgericht en gedoseerd inzet. Voor „taai“ of instabiel gedrag zijn vrijwel altijd twee oorzaken verantwoordelijk: de Main Thread wacht ergens blokkerend – of hij verdrinkt in te veel queued Updates.
Als u het patroon eenmaal netjes hebt opgezet (Counter/Coalescing, Cancel-Flag, Abschluss-Callback), levert het op veel plekken in een gegroeide Delphi-applicatie voordeel op: Import/Export, gegevenscontroles, bestand- en API-jobs – alles wordt responsiever, zonder dat u zich bij elk Progress-Update nieuwe deadlocks op de hals haalt.
Als u ondersteuning nodig heeft bij het stabiliseren van parallelle code, bij het debuggen van vastlopers in de UI of bij het zorgvuldig moderniseren van gegroeide Delphi-applicaties: neem contact op.
Voor dit onderwerp zijn ook Delphi Parallel Programming Library en Tthread.queue Vs Synchronize belangrijk. Het artikel ordent deze aspecten begrijpelijk en toont waarop het in de dagelijkse praktijk aankomt.
volgende stap
Wanneer het onderwerp een concreet project wordt, moeten architectuur, bestaande omgeving en exploitatie vroegtijdig samen worden bekeken.
We ondersteunen niet alleen bij individuele vragen, maar ook wanneer uit broncodefragmenten, legacy-onderwerpen of portalideeën een robuust bedrijfsproject moet ontstaan.
- Huidige situatie, doelbeeld en technische risico's worden gezamenlijk beoordeeld.
- REST, toegang tot gegevens, portalen en rollout worden niet naar latere fasen verschoven.
- U ziet vroeg welke weg economisch en operationeel levensvatbaar is.