Vom Magazinthema zur Projektpraxis
Passende Leistungs- und Technikseiten zum Beitrag
Wer in Delphi rechenintensive oder I/O-lastige Jobs parallelisieren will, landet schnell bei der Parallel Programming Library (PPL) und konkret bei TParallel.For. Der Effekt ist oft sofort messbar – bis man im selben Atemzug eine Progress-UI „mal eben“ aktualisieren möchte. Genau hier entstehen die typischen Hänger: scheinbar zufällige UI-Freezes, eine ProgressBar, die rückwärts springt, oder ein kompletter Deadlock, sobald man im Debugger Schritt für Schritt läuft.
In diesem Beitrag geht es um TParallel.For threadsichere Progress-UI: ein robustes Muster, das in VCL und FMX funktioniert, UI-Updates über TThread.Queue sauber bündelt, Cancel/Abort berücksichtigt und die häufigsten Deadlock-Fallen konsequent umgeht. Der Fokus liegt auf Betriebsrealität: reproduzierbares Verhalten, verständliche Zuständigkeiten und Debugging-Hinweise, die auch dann helfen, wenn der Fehler nur „bei Kunden“ auftritt.
Warum Progress-UI bei TParallel.For so oft schiefgeht
TParallel.For läuft typischerweise auf Worker-Threads aus dem Delphi-Threadpool. Diese Threads dürfen keine VCL- oder FMX-Controls direkt anfassen, weil die UI-Frameworks (Message Loop, Window Handles, Rendering) an den Main Thread gebunden sind. Schon ein scheinbar harmloses ProgressBar.Position := … aus einem Worker kann zu undefiniertem Verhalten führen: sporadische AVs, eingefrorene Fenster oder „flackernde“ Updates.
Die naheliegende Reparatur ist oft TThread.Synchronize. Das behebt zwar die Thread-Safety, führt aber in parallelen Schleifen schnell zu einem anderen Problem: Sie erzeugen einen seriellen Engpass. Jeder Worker wartet auf den UI-Thread, der wiederum mit dem Rendern und dem Abarbeiten von Synchronize-Aufrufen beschäftigt ist. Unter Last sieht das wie ein Deadlock aus – auch wenn es „nur“ ein starvation/lockstep-Effekt ist.
Und dann gibt es die echte Deadlock-Klasse: Der Main Thread wartet (z. B. per WaitFor, Task.Wait oder indirekt über blockierende Aufrufe) auf das Ende der Parallel-Operation, während Worker-Threads versuchen, per Synchronize oder ungünstiger Queue-Nutzung Arbeit in den Main Thread zu schicken. Ergebnis: Main Thread wartet auf Worker, Worker warten auf Main Thread.
TThread.Queue vs. TThread.Synchronize: der praktische Unterschied
Beide Mechanismen dienen dazu, Code sicher im Main Thread auszuführen. Der Unterschied ist die Wartesemantik:
- TThread.Synchronize: Der aufrufende Worker wartet, bis der Main Thread den Code ausgeführt hat. Das ist „synchron“, erhöht Latenzen und ist eine klassische Zutat für Deadlocks, wenn der Main Thread gerade blockiert.
- 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
Ein robustes Muster besteht aus drei klar getrennten Verantwortlichkeiten:
- Worker-Threads machen die eigentliche Arbeit pro Element/Index. Sie melden nur Fortschritt in einer threadsicheren Form (Zähler, Queue, Thread-safe Queue).
- Aggregator (oft: Main Thread oder ein dedizierter Timer im UI) berechnet aus dem Fortschritt einen UI-Status (Position, Text, ETA) und aktualisiert Controls. So vermeiden Sie 1:1-Updates pro Iteration.
- Abschluss (ebenfalls im Main Thread): UI reaktivieren, Ergebnis anzeigen, Fehler zusammenfassen, Ressourcen freigeben.
Warum diese Trennung so gut funktioniert: Der Workload kann hochfrequent sein (tausende Elemente), aber die UI braucht nur wenige Updates pro Sekunde. In der Praxis reichen 5–10 Updates/Sekunde, bei sehr schnellen Jobs sogar 2–4. Alles darüber ist meist nur optischer Lärm und kostet CPU-Zeit im Message Pump.
Threadsicher zählen: Atomic statt Lock
Für eine einfache ProgressBar reicht oft ein atomarer Zähler. „Atomar“ heißt: Inkrement und Read passieren ohne Race Condition, typischerweise über TInterlocked. Damit vermeiden Sie Locks (Critical Sections) im Hot Path der Schleife.
Bewährte Minimalidee:
- Gesamtmenge ist vorab bekannt (z. B. Anzahl Datensätze, Dateien, IDs).
- Jede Iteration erhöht einen DoneCounter atomar.
- Ein UI-Timer liest den Counter periodisch und setzt ProgressBar.Position.
Vorteil: Kein TThread.Queue pro Element, keine UI-Überflutung. Nachteil: Sie haben keine Detailmeldungen pro Element (z. B. Dateiname). Dafür kann man eine zweite, gedrosselte Statusmeldung ergänzen (siehe nächster Abschnitt).
Statusmeldungen ohne Spam: „letzter Status gewinnt“
Wenn Sie zusätzlich einen kurzen Text anzeigen wollen (aktuelles Element, Phase, Fehlermeldung), braucht es ebenfalls ein Muster, das nicht bei jedem Worker-Schritt die UI flutet. In der Praxis funktioniert „letzter Status gewinnt“ sehr gut:
- Worker schreibt eine Status-Information in eine threadsichere Struktur (z. B. atomar austauschbarer String, oder geschützt per kleinem Lock).
- UI-Timer übernimmt periodisch den zuletzt gesehenen Status in ein Label.
Damit bleibt die UI reaktionsfähig, und Sie sehen trotzdem, dass „etwas passiert“. Wichtig ist hier weniger der String selbst als die Lebensdauer: keine Referenzen auf kurzlebige Objekte aus Worker-Threads in den UI-Thread schleppen. Wenn Sie Objekte übergeben, klären Sie Ownership explizit.
TParallel.For threadsichere Progress-UI mit TThread.Queue: ein robustes Muster
Es gibt Szenarien, in denen ein UI-Timer allein nicht reicht: z. B. wenn Sie am Ende sicher genau ein „Fertig“-Update auslösen wollen, oder wenn das UI-Update ein komplexerer Schritt ist (z. B. Eintrag in ein Log-Fenster, aber gedrosselt). Dann ist TThread.Queue passend – aber eben nicht pro Iteration, sondern gezielt.
Ein praxistauglicher Ansatz ist eine Queue nur für Ereignisse mit geringer Frequenz:
- Start-Event (UI vorbereiten, Buttons sperren)
- Periodische Progress-Events (max. alle X Millisekunden)
- Fehler-Events (optional gesammelt)
- Done-Event (UI zurücksetzen, Ergebnis anzeigen)
Die Periodik erreichen Sie nicht über den UI-Thread, sondern bereits im Worker-Kontext: Sie lassen Worker nur dann ein UI-Update queuen, wenn seit dem letzten UI-Update ausreichend Zeit vergangen ist. Dazu eignet sich eine monotone Zeitquelle (z. B. TickCount) plus atomarer „last update“-Wert.
Wichtig: Das UI-Update selbst muss „schnell“ sein. Teure Berechnungen, Datei-I/O oder Datenbankzugriffe gehören nicht in den gequeueten UI-Callback. Der Callback sollte nur Zustände auslesen und Controls setzen.
Cancel-Handling: Abbrechen ohne Hänger
In echten Anwendungen ist Abbrechen nicht optional. Entscheidend ist: Cancel ist kein „Kill“, sondern ein kooperatives Beenden. Worker müssen regelmäßig prüfen, ob ein Abbruchsignal gesetzt ist, und dann sauber aussteigen. In Delphi gibt es dafür mehrere Wege (je nach PPL-Konstruktion): ein eigenes Volatile-Flag, ein atomarer Boolean, oder ein Cancellation-Konzept über Tasks (je nach Delphi-Version und Struktur).
Für den Betrieb wichtig sind zwei Regeln:
- Cancel muss schnell sichtbar werden: Prüfen Sie das Abbruchflag an sinnvollen Stellen, nicht nur am Ende einer Iteration, wenn die Iteration auch mal Sekunden dauern kann.
- Cancel muss aufräumen: Offene 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.
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.
- Fehlerbehandlung: Exceptions aus Worker-Threads müssen eingesammelt werden. In der Praxis bewährt sich: erste Exception merken und Cancel auslösen, oder alle Exceptions sammeln und am Ende zusammengefasst anzeigen.
- Reihenfolge: Wenn die Ausgabe eine stabile Reihenfolge braucht (z. B. Logs nach Index), ist parallele Verarbeitung mit späterem Sortieren oft einfacher als „thread-sicheres geordnetes Einfügen“.
Für die UI heißt das: Zeigen Sie nicht jede einzelne Fehlermeldung sofort. Das führt zu Modal-Dialog-Hölle. Sammeln Sie Fehler (z. B. Liste von Strings), und zeigen Sie am Ende eine Zusammenfassung oder ein exportierbares Log.
Debugging: Wie Sie den Deadlock wirklich sichtbar machen
Deadlocks in Parallel-Code sind frustrierend, weil sie im Debugger anders aussehen können als im Release. Trotzdem gibt es ein paar sehr praktische Hebel:
Threads-Fenster und Call Stacks nutzen
Wenn die UI hängt, schauen Sie sich alle Threads an: Wo steht der Main Thread? Wartet er? Ist er in einer Message-Loop? Wo stehen Worker-Threads? Wenn Worker in Synchronize hängen, ist die Ursache fast immer „Main Thread blockiert“ oder „Main Thread braucht einen Lock“.
Queue-/Synchronize-Stellen markieren
Setzen Sie gezielt Logging an Übergabestellen (vor dem Queue, im Queue-Callback, am Ende der Iteration). Im Betrieb ist das oft wertvoller als Breakpoints, weil Timing entscheidend ist. Achten Sie darauf, dass Logging selbst thread-sicher und nicht blockierend ist (z. B. keine UI-Log-Ausgabe direkt aus Worker-Threads).
Last-Update-Zeit messen
Wenn die UI „hängt“, kann es schlicht sein, dass sie zu viele Updates abarbeiten muss. Messen Sie daher im Main Thread, wie oft Sie pro Sekunde ein UI-Update ausführen und wie lange es dauert. Sobald UI-Callbacks mehr als wenige Millisekunden brauchen, ist Drosselung oder Vereinfachung nötig.
Wann lohnt sich TParallel.For mit Progress-UI wirklich?
Parallelisierung ist kein Selbstzweck. Sie lohnt sich besonders, wenn:
- die Iterationen groß genug sind (Millisekunden bis Sekunden), sodass Threadpool-Overhead untergeht,
- der Job CPU-lastig ist (Parsing, Kompression, Hashing) oder gut parallelisierbare I/O hat (mehrere Dateien, mehrere HTTP-Requests mit Limits),
- Sie eine klare Cancel- und Fehlerstrategie haben,
- die UI-Anforderungen mit aggregiertem Progress auskommen.
Sie lohnt sich weniger, wenn jede Iteration extrem kurz ist (Mikro-Operationen) oder wenn alle Iterationen auf denselben Engpass laufen (eine serielle DB-Transaktion, ein globaler Lock, eine einzelne Datei). Dann ist der schnellere Hebel oft: Algorithmus verbessern, Batchen, Datenzugriff reduzieren oder den Engpass explizit entkoppeln.
Praxis-Checkliste: So bleibt die UI stabil
- Main Thread blockiert nicht: keine Waits, keine langen Loops ohne Message Pump.
- Worker fassen nie Controls an: keine VCL/FMX-Zugriffe außerhalb des UI-Threads.
- UI-Updates sind gedrosselt: Counter/Timer oder coalesced Queue-Updates statt pro Iteration.
- Keine Synchronize-Aufrufe aus Locks heraus.
- Cancel ist kooperativ, häufig geprüft und räumt sauber auf.
- Exceptions werden gesammelt und am Ende geordnet behandelt.
Fazit: Mit Queue entkoppeln, mit Aggregation stabilisieren
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.
nächster Schritt
Wenn aus dem Thema ein reales Projekt wird, sollten Architektur, Bestand und Betrieb früh zusammen betrachtet werden.
Wir unterstuetzen nicht nur bei Einzelfragen, sondern auch dann, wenn aus Source-Schnipseln, Legacy-Themen oder Portalideen ein belastbares Unternehmensprojekt werden soll.
- Bestand, Zielbild und technische Risiken werden zusammen bewertet.
- REST, Datenzugriff, Portale und Rollout werden nicht als Spätfolgen verschoben.
- Sie sehen früh, welcher Weg wirtschaftlich und betrieblich tragfähig ist.