Net-Base Magasin

24.07.2026

Delphi: TParallel.For med trådsäker Progress-UI (TThread.Queue) utan deadlocks

Så kombinerar du Delphi TParallel.For med en trådsäker Progress-UI: uppdateringar via TThread.Queue, ren aggregering, avbrytshantering och typiska deadlock-fällor vid felsökning och drift.

24.07.2026

Från magasinets tema till projektpraxis

Passande tjänste- och tekniksidor för inlägget

Den som i Delphi vill parallellisera beräkningsintensiva eller I/O-tunga jobb hamnar snabbt vid Parallel Programming Library (PPL) och konkret vid TParallel.For. Effekten är ofta omedelbart mätbar – tills man i samma andetag vill uppdatera en Progress-UI „bara snabbt“. Precis här uppstår de typiska hängningarna: till synes slumpmässiga UI-frysningar, en ProgressBar som hoppar bakåt, eller en komplett deadlock så snart man stegvis kör i debuggern.

I det här inlägget handlar det om TParallel.For trådsäker Progress-UI: ett robust mönster som fungerar i VCL och FMX, samlar UI-uppdateringar via TThread.Queue på ett ordnat sätt, tar hänsyn till Cancel/Abort och undviker konsekvent de vanligaste deadlock-fällorna. Fokus ligger på driftrealism: reproducerbart beteende, tydliga ansvarsområden och debugging-anvisningar som hjälper även när felet bara uppstår „hos kund“.

Varför Progress-UI med TParallel.For så ofta går fel

TParallel.For körs typiskt på arbets‑trådar från Delphi-threadpoolen. Dessa trådar får inte röra VCL- eller FMX-kontroller direkt, eftersom UI-ramverken (Message Loop, Window Handles, Rendering) är bundna till huvudtråden. Redan en till synes harmlös ProgressBar.Position := … från en arbets‑tråd kan leda till odefinierat beteende: sporadiska AVs, frusna fönster eller „fladdrande“ uppdateringar.

Den självklara åtgärden är ofta TThread.Synchronize. Det åtgärdar visserligen trådsäkerheten, men leder i parallella slingor snabbt till ett annat problem: det skapar en seriell flaskhals. Varje arbets‑tråd väntar på UI-tråden, som i sin tur är upptagen med rendering och hantering av Synchronize-anrop. Under belastning ser det ut som en deadlock – även om det „bara“ är en starvation/lockstep-effekt.

Och sedan finns den verkliga deadlock-klassen: huvudtråden väntar (t.ex. per WaitFor, Task.Wait eller indirekt via blockerande anrop) på slutet av den parallella operationen, medan arbets‑trådar försöker skicka arbete till huvudtråden via Synchronize eller ogynnsam Queue-användning. Resultat: huvudtråden väntar på arbets‑trådar, arbets‑trådarna väntar på huvudtråden.

TThread.Queue vs. TThread.Synchronize: den praktiska skillnaden

Unschärfer Debugger-Blick mit Notizen zu Threads und Queue als Kontext für Queue vs Synchronize
Vid debugging ser man snabbt om arbets‑trådar väntar på huvudtråden eller bara köar uppdateringar.

Båda mekanismerna tjänar till att köra kod säkert i huvudtråden. Skillnaden är väntesemantiken:

  • TThread.Synchronize: Den anropande arbets‑tråden väntar tills huvudtråden har kört koden. Det är „synkron“, ökar latenser och är en klassisk orsak till deadlocks när huvudtråden är blockerad.
  • TThread.Queue: Worker placerar bara koden i en kö för huvudtråden och fortsätter köra. Det är „asynkront“, lösgör trådar och är i parallella scenarier nästan alltid det bättre standardvalet – förutsatt att man kontrollerar uppdateringsfrekvensen.

Viktigt: Queue är ingen fribiljett. Om du skickar en Queue-uppdatering i varje iteration av en loop översvämmer du huvudtrådens kö. Då fastnar inte UI på grund av deadlock, men på grund av ren mängd meddelanden. UI:n upplevs som „seg“, och slutet av bearbetningen fördröjs eftersom hundratals eller tusentals UI-uppdateringar fortfarande körs efteråt.

Det fall som verkligen gör ont: väntan i UI-tråden

I företagsapplikationer ser man ofta följande förlopp: man klickar på knappen „Start“, UI inaktiveras, ProgressDialog öppnas, därefter väntar man synkront tills allt är klart för att därefter återaktivera. Detta mönster är kärnan i många deadlocks.

Typiska varianter (beroende på kodbas):

  • Huvudtråden startar TParallel.For och anropar därefter en blockerande väntelogik (direkt eller indirekt).
  • En ProgressDialog anropar i Constructor eller OnShow en rutin som internt väntar.
  • En Avbryt-knapp sätter visserligen en flagga, men huvudtråden fastnar ändå i en vänteloop.

Om Worker-Threads under denna tid använder Synchronize är deadlock i praktiken garanterat. Med Queue kan det också fastna om huvudtråden är blockerad och inte pumpar meddelanden – för då bearbetas inte heller Queue.

Den robusta konsekvensen är: Huvudtråden får inte blockera medan den väntar på parallellslingan om parallella UI-uppdateringar krävs. Istället måste bearbetningen antingen helt flyttas till en bakgrunds-Task, eller så organiserar man ett „asynkront slut“ (Callback/Queued avslutningsaktion) som återfrigör UI i slutet.

Ren ansats: Progress endast aggregerat, UI-uppdateringar drosslade

Arbetsplatsmotiv med abstraherad progressindikator och tidmätning som symbol för drosslade UI-uppdateringar
Aggregering och tidsbaserad drossling förhindrar att UI översvämmas av för många uppdateringar.

Ett robust mönster består av tre tydligt åtskilda ansvarsområden:

  • Worker-Threads utför det faktiska arbetet per element/index. De rapporterar endast framsteg i en trådsäker form (räknare, Queue, trådsäker Queue).
  • Aggregator (ofta: huvudtråden eller en dedikerad timer i UI) beräknar utifrån framstegen en UI-status (position, text, ETA) och uppdaterar kontroller. På så sätt undviker du 1:1-uppdateringar per iteration.
  • Avslut (också i huvudtråden): reaktivera UI, visa resultat, sammanfatta fel, frigöra resurser.

Varför denna separation fungerar så bra: Workloaden kan vara högfrekvent (tusentals element), men UI behöver bara några uppdateringar per sekund. I praktiken räcker 5–10 uppdateringar/sekund, vid mycket snabba jobb till och med 2–4. Allt därutöver är oftast bara visuellt brus och kostar CPU-tid i meddelandepumpen.

Trådsäkert räkna: atomärt istället för lås

För en enkel ProgressBar räcker ofta en atomär räknare. „Atomär“ betyder: inkrement och läsning sker utan race condition, typiskt via TInterlocked. På så sätt undviker du lås (kritiska sektioner) i loopens hot path.

Beprövad minimalidé:

  • Totalmängden är känd i förväg (t.ex. antal poster, filer, ID:n).
  • Varje iteration ökar atomärt en DoneCounter.
  • En UI-timer läser countern periodiskt och sätter ProgressBar.Position.

Fördel: Ingen TThread.Queue per element, ingen UI-överbelastning. Nackdel: Du har inga detaljmeddelanden per element (t.ex. filnamn). Som komplement kan man lägga till ett andra, nedtonat statusmeddelande (se nästa avsnitt).

Statusmeddelanden utan spam: „sista status vinner“

Om du dessutom vill visa en kort text (aktuellt element, fas, felmeddelande) krävs också ett mönster som inte flödar UI vid varje workersteg. I praktiken fungerar „sista status vinner“ mycket väl:

  • Workern skriver en statusinformation till en trådsäker struktur (t.ex. atomärt utbytbar String, eller skyddad med ett litet lås).
  • UI-timern för över periodiskt den senast sedda statusen till en Label.

Detta håller UI responsivt, och du ser ändå att „något händer“. Viktigare än själva strängen är livslängden: släpa inte referenser till kortlivade objekt från worker-trådar in i UI-tråden. Om du skickar objekt, fastställ ägarskapet uttryckligen.

TParallel.For trådsäker Progress-UI med TThread.Queue: ett robust mönster

Det finns scenarier där en UI-timer inte räcker: till exempel när du i slutet säkert vill utlösa exakt en „Fertig“-uppdatering, eller när UI-uppdateringen är ett mer komplext steg (t.ex. post i ett loggfönster, men drosslat). Då passar TThread.Queue – men inte per iteration, utan selektivt.

Ett praktiskt tillvägagångssätt är en kö endast för händelser med låg frekvens:

  • Start-Event (förbered UI, lås knappar)
  • Periodiska Progress-Events (max var X millisekund)
  • Fehler-Events (valfritt samlade)
  • Done-Event (återställ UI, visa resultat)

Periodiken hanterar du inte i UI-tråden, utan redan i worker-kontexten: du låter workern bara lägga ett UI-uppdateringsanrop i kön om tillräcklig tid förflutit sedan senaste UI-uppdateringen. För detta passar en monoton tidskälla (t.ex. TickCount) plus ett atomärt „last update“-värde.

Viktigt: Själva UI-uppdateringen måste vara „snabb“. Kostsamma beräkningar, fil-I/O eller databasåtkomst hör inte hemma i den köade UI-callbacken. Callbacken bör bara läsa tillstånd och sätta kontroller.

Cancel-Handling: avbryt utan hängning

I verkliga applikationer är avbryt inte valfritt. Avgörande är: Cancel är ingen „Kill“, utan ett kooperativt avslut. Workrar måste regelbundet kontrollera om ett avbrottssignal har satts, och sedan avsluta på ett ordnat sätt. I Delphi finns det flera vägar för detta (beroende på PPL-konstruktion): en egen Volatile-flagga, en atomär Boolean, eller ett Cancellation-koncept över Tasks (beroende på Delphi-version och struktur).

För driften är två regler viktiga:

  • Avbryt måste bli synligt snabbt: Kontrollera avbrottsflaggan på lämpliga ställen, inte bara i slutet av en iteration när iterationen kan ta flera sekunder.
  • Avbryt måste städa upp: Öppna Handles, temporära filer, transaktioner eller lås får inte lämnas kvar. Det innebär: i varje Worker-iteration är try/finally-block obligatoriska när resurser är involverade.

I UI:n bör Avbryt endast sätta en signal och föra UI till ett „Stopping…“-tillstånd. Den faktiska avslutningen och återaktiveringen av UI sker sedan i Done-eventet, inte omedelbart vid klicket.

Undvik deadlocks: die häufigsten Fallen in der Praxis

Diagramm eines zyklischen Wartens zwischen Threads als Visualisierung eines Deadlocks
Deadlocks uppstår ofta genom cyklisk väntan: UI-tråden blockeras, Worker väntar på åtkomst till UI.

Fälla 1: WaitFor/Task.Wait i huvudtråden

När huvudtråden blockeras kan den inte köra Queue-callbacks eller bearbeta meddelanden. Det uppträder som ett deadlock även om Worker fortsätter korrekt. Lösning: inga blockerande waits i UI-tråden. Istället avslutningsåtgärd via TThread.Queue eller en händelsestyrning (t.ex. timer som kontrollerar „klar“).

Fälla 2: Synchronize inuti ett lås

En klassiker: en Worker håller en kritisk sektion, anropar sedan Synchronize, och i UI-callbacken krävs (direkt eller indirekt) samma kritiska sektion igen. Resultat: cirkulärt väntande. Regeln är enkel: Ingen UI-överlämning (Synchronize/Queue) från ett hållit lås. Om ett lås är nödvändigt, hämta alla data först i lokala variabler, lämna låset, och queue:a sedan.

Fälla 3: UI-callback orsakar reentrancy

Ibland är själva UI-uppdateringen inte „harmlös“: att sätta Properties kan trigga Events (OnChange, OnResize) som i sin tur startar logik som går åt Worker-tillstånd. Det är inte ett deadlock i strikt mening, men leder till svårförklarliga frysningar och race conditions. Åtgärd: placera UI-uppdateringar i „tysta“ banor (deaktivera Events temporärt) eller använd reentrancy-guards (t.ex. atomär guard för uppdateringsfasen).

Fälla 4: För många queued-uppdateringar

Även utan Waits kan UI:n „stanna“ om du genererar tiotusentals queued callbacks. Symptom: ProgressBar fortsätter länge efteråt, fönstret reagerar trögt, CPU i huvudtråden hög. Lösning: dämpa (tidsfönster), aggregera (räknare), eller en riktig Producer/Consumer-struktur där endast en UI-uppdatering kan vara „pending“ (Coalescing).

När det blir mer komplicerat: samla resultat, aggregera fel, garantera ordningsföljd

TParallel.For är idealiskt när iterationerna är oberoende. I affärsprogram är iterationerna dock ofta bara „till stor del“ oberoende: de läser filer, anropar REST-APIs, skriver databaserader. Då måste ni planera tre ytterligare punkter noggrant:

  • Trådsäker resultatinsamling: Antingen per tråd en lokal buffert (i slutet slå ihop) eller en trådsäker Queue/Collection. Undvik lås i Hot Path.
  • Fehlerbehandlung: Undantag från worker-trådar måste samlas in. I praktiken fungerar det att: komma ihåg det första undantaget och utlösa avbrytning, eller samla alla undantag och visa dem sammanfattade i slutet.
  • Reihenfolge: Om utdata kräver en stabil ordning (t.ex. loggar enligt index) är parallell bearbetning med efterföljande sortering ofta enklare än ett „trådsäkert ordnat infogande“.

För UI innebär det: Visa inte varje enskilt felmeddelande omedelbart. Det leder till ett helvete med modala dialoger. Samla fel (t.ex. en lista med strängar) och visa i slutet en sammanfattning eller en exporterbar logg.

Debugging: Wie Sie den Deadlock wirklich sichtbar machen

Deadlocks i parallellkod är frustrerande eftersom de kan se annorlunda ut i debug än i release. Trots det finns några mycket praktiska åtgärder:

Threads-Fenster und Call Stacks nutzen

Om UI:n hänger, titta på alla trådar: Var befinner sig huvudtråden? Väntar den? Är den i en meddelandeloop? Var finns worker-trådarna? Om worker fastnar i Synchronize är orsaken nästan alltid „huvudtråden blockerad“ eller „huvudtråden behöver en låsning“.

Queue-/Synchronize-Stellen markieren

Placera riktad loggning vid överlämningspunkter (före kön, i kö-callbacken, i slutet av iterationen). I drift är det ofta mer värdefullt än breakpoints eftersom timing är avgörande. Se till att loggningen själv är trådsäker och icke-blockerande (t.ex. ingen UI-loggutskrift direkt från worker-trådar).

Last-Update-Zeit messen

Om UI:n „hänger“ kan det helt enkelt bero på att den måste bearbeta för många uppdateringar. Mät därför i huvudtråden hur ofta du per sekund utför en UI-uppdatering och hur lång tid den tar. Så snart UI-callbacks tar mer än några millisekunder behövs dämpning eller förenkling.

Wann lohnt sich TParallel.For mit Progress-UI wirklich?

Parallellisering är inget självändamål. Den är särskilt motiverad när:

  • iterationerna är tillräckligt stora (millisekunder till sekunder), så att threadpool-overhead uppvägs,
  • jobbet är CPU-intensivt (Parsing, Kompression, Hashing) eller har väl parallelliserbar I/O (flera filer, flera HTTP-requests med begränsningar),
  • du har en tydlig avbrotts- och felstrategi,
  • UI-kraven kan hanteras med aggregerad progress.

Den är mindre lönsam när varje iteration är extremt kort (mikrooperationer) eller när alla iterationer träffar samma flaskhals (en seriell DB-transaktion, ett globalt lås, en enskild fil). Då är det ofta ett snabbare grepp att: förbättra algoritmen, batcha, minska datatillgång eller explicit koppla bort flaskhalsen.

Praxis-Checkliste: So bleibt die UI stabil

  • Huvudtråden är inte blockerad: inga waits, inga långa loopar utan message pump.
  • Worker rör aldrig Controls: inga VCL/FMX-åtkomster utanför UI-tråden.
  • UI-uppdateringar är dämpade: Counter/Timer eller sammanslagna kö-uppdateringar istället för per iteration.
  • Inga Synchronize-anrop från inom lås.
  • Avbrytning är kooperativ, kontrolleras ofta och städar upp ordentligt.
  • Undantag samlas in och hanteras ordnat i slutet.

Fazit: Mit Queue entkoppeln, mit Aggregation stabilisieren

En robust progress‑UI vid TParallel.For uppstår inte genom att stoppa in „Synchronize“ varsomhelst, utan genom en tydlig arkitekturprincip: worker arbetar oberoende, huvudtråden förblir fri och hanterar endast ett fåtal snabba UI‑uppdateringar. TThread.Queue är då rätt verktyg när du använder det målmedvetet och reglerat. För „trögt“ eller instabilt beteende är det nästan alltid två orsaker: huvudtråden väntar blockerande någonstans – eller den drunknar i för många köade uppdateringar.

När du väl satt upp mönstret rent (Counter/Coalescing, Cancel-Flag, Abschluss-Callback) lönar det sig över många delar av en befintlig Delphi-applikation: import/export, datakontroller, fil- och API-jobb – allt blir mer responsivt utan att du riskerar nya deadlocks vid varje Progress-Update.

Behöver du stöd för att stabilisera parallellkod, för felsökning av UI‑hänger eller för en ren modernisering av befintliga Delphi-applikationer: ta kontakt.

För detta ämne är även Delphi Parallel Programming Library och Tthread.queue Vs Synchronize viktiga. Inlägget placerar dessa aspekter i ett begripligt sammanhang och visar vad som är avgörande i vardagen.

Diskutera projekt eller moderniseringsprojekt med Net-Base.

nästa steg

När ett ämne blir ett verkligt projekt bör arkitektur, befintligt bestånd och drift tidigt ses över gemensamt.

Vi stöder inte bara vid enstaka frågor, utan även när kodsfragment, legacy-frågor eller portalidéer ska utvecklas till ett robust företagsprojekt.

  • Nuläge, målbild och tekniska risker bedöms tillsammans.
  • REST, dataåtkomst, portaler och utrullning skjuts inte upp som sena följder.
  • Ni ser tidigt vilken väg som är ekonomiskt och driftmässigt hållbar.

Dela inlägg

Dela det här inlägget direkt

LinkedIn, X, XING, Facebook, WhatsApp och e-post är omedelbart tillgängliga. För Instagram förbereder vi länken och en kort text direkt.

E-post

Instagram öppnas i en ny flik. Länken och korttexten kopieras till urklipp först.