Net-Base Magasin

24.07.2026

Delphi: TParallel.For med trådsikker Progress-UI (TThread.Queue) uden deadlocks

Sådan kombinerer du Delphi TParallel.For med en trådsikker Progress-UI: opdateringer via TThread.Queue, ren aggregation, annulleringshåndtering og typiske deadlock-fælder under debugging og drift.

24.07.2026

Fra magasinets tema til projektpraksis

Passende service- og tekniske sider til artiklen

Den, der i Delphi vil parallellisere beregningsintensive eller I/O-tunge job, ender hurtigt hos Parallel Programming Library (PPL) og konkret TParallel.For. Effekten er ofte øjeblikkelig – indtil man i samme åndedrag forsøger at opdatere en progress‑UI „lige hurtigt“. Netop her opstår de typiske hengere: tilsyneladende tilfældige UI‑frysninger, en ProgressBar, der hopper baglæns, eller et komplet deadlock, så snart man kører trinvis i debuggeren.

I dette indlæg handler det om TParallel.For trådsikker Progress-UI: et robust mønster, der fungerer i VCL og FMX, samler UI‑opdateringer via TThread.Queue på en ordentlig måde, tager højde for Cancel/Abort og undgår de mest almindelige deadlock‑faldgruber konsekvent. Fokus er på driftsrealitet: reproducerbar adfærd, klare ansvarsfordelinger og debugging‑anvisninger, som også hjælper, når fejlen kun optræder „hos kunder“.

Warum Progress-UI bei TParallel.For so oft schiefgeht

TParallel.For kører typisk på worker‑tråde fra Delphi-threadpoolen. Disse tråde må ikke røre ved VCL- eller FMX-controls direkte, fordi UI‑frameworks (Message Loop, Window Handles, Rendering) er bundet til Main‑Thread. Selv en tilsyneladende harmløs ProgressBar.Position := … fra en worker kan føre til udefineret opførsel: sporadiske AVs, frosne vinduer eller „flimrende“ opdateringer.

Den oplagte løsning er ofte TThread.Synchronize. Det løser visse trådsikkerhedsproblemer, men fører i parallelle løkker hurtigt til et andet problem: det skaber en serielflaskehals. Hver worker venter på Main‑Thread, som på sin side er optaget af rendering og afvikling af Synchronize‑kald. Under belastning ligner det et deadlock – selvom det „kun“ er en starvation/lockstep‑effekt.

Og så er der den egentlige deadlock‑klasse: Main‑Thread venter (f.eks. via WaitFor, Task.Wait eller indirekte gennem blokerende kald) på slutningen af den parallelle operation, mens worker‑tråde forsøger at sende arbejde til Main‑Thread via Synchronize eller uhensigtsmæssig Queue-brug. Resultat: Main‑Thread venter på worker, worker venter på Main‑Thread.

TThread.Queue vs. TThread.Synchronize: der praktische Unterschied

Uklart debuggerblik med noter om tråde og Queue som kontekst for Queue vs Synchronize
Ved debugging ser man hurtigt, om worker‑tråde venter på Main‑Thread eller blot placerer queued opdateringer.

Begge mekanismer har til formål at køre kode sikkert i Main‑Thread. Forskellen ligger i ventesemantikken:

  • TThread.Synchronize: Den kaldende worker venter, indtil Main‑Thread har kørt koden. Det er „synkront“, øger latenser og er en klassisk ingrediens i deadlocks, hvis Main‑Thread er blokeret.
  • TThread.Queue: Arbejdertråden placerer kun koden i en kø for hovedtråden og fortsætter. Det er „asynkront“, løsner trådene fra hinanden og er i parallelle scenarier næsten altid det bedre standardvalg – forudsat at man kontrollerer opdateringsfrekvensen.

Vigtigt: Queue er ikke et frikort. Hvis du i hver iteration af en løkke sender en Queue-opdatering, oversvømmer du hovedtrådens Queue. Så hænger UI’en ikke på grund af en deadlock, men på grund af det rene antal beskeder. UI’en virker „træg“, og afslutningen af behandlingen forsinkes, fordi flere hundrede eller tusinder af UI-opdateringer stadig kører efter.

Det Randtilfælde, der virkelig gør ondt: Venten i UI-tråden

I virksomhedsapplikationer ser man ofte følgende forløb: Knappen „Start“ klikkes, UI’en deaktiveres, en ProgressDialog vises, og der ventes synkront, indtil alt er færdigt, hvorefter UI’en aktiveres igen. Dette mønster er kernen i mange deadlocks.

Typiske varianter (afhængig af kodebasen):

  • Hovedtråden starter TParallel.For og kalder derefter en blokerende ventelogik (direkte eller indirekte).
  • En ProgressDialog kalder i Constructor eller OnShow en rutine, der internt venter.
  • Annuller-knappen sætter visserligen et flag, men hovedtråden forbliver alligevel fanget i en venteløkke.

Hvis worker-tråde i denne periode Synchronize bruger, er deadlock praktisk talt garanteret. Med Queue kan det også fryse, hvis hovedtråden er blokeret og ikke pumper beskeder – for så bliver Queue’en heller ikke behandlet.

Den driftssikre konsekvens er: Hovedtråden må ikke blokerende vente på den parallelle løkke, hvis der parallelt kræves UI-opdateringer. I stedet skal behandlingen enten fuldstændigt udlægges til en baggrundsopgave, eller man organiserer en „asynkron ende“ (Callback/Queued Abschlussaktion), der frigiver UI’en til sidst.

Korrekt tilgang: Fremdrift kun aggregeret, UI-opdateringer begrænset

Arbeitsplatzmotiv mit abstrahierter Progress-Anzeige und Zeitmessung als Symbol für gedrosselte UI-Updates
Aggregation und zeitbasierte Drosselung verhindern, dass die UI durch zu viele Updates überflutet wird.

Et robust mønster består af tre klart adskilte ansvarsområder:

  • Worker-Threads udfører det egentlige arbejde pr. element/index. De rapporterer kun fremdrift i en trådsikker form (tæller, Queue, trådsikker Queue).
  • Aggregator (ofte: hovedtråden eller en dedikeret timer i UI) beregner ud fra fremdriften en UI-status (position, tekst, ETA) og opdaterer kontrolelementer. På den måde undgår du 1:1-opdateringer pr. iteration.
  • Abschluss (også i hovedtråden): genaktivere UI’en, vise resultatet, opsummere fejl, frigive ressourcer.

Hvorfor denne adskillelse fungerer så godt: Arbejdsmængden kan være højfrekvent (tusindvis af elementer), men UI’en har kun brug for få opdateringer pr. sekund. I praksis er 5–10 opdateringer pr. sekund tilstrækkeligt, ved meget hurtige job endda 2–4. Alt derover er som regel blot visuel støj og koster CPU-tid i Message Pump.

Trådsikker optælling: Atomisk i stedet for Lock

Til en simpel ProgressBar er en atomar tæller ofte tilstrækkelig. ”Atomar” betyder: inkrement og læsning sker uden race condition, typisk via TInterlocked. Dermed undgår man locks (Critical Sections) i hot path’en af løkken.

Velafprøvet minimalidé:

  • Det samlede antal er kendt på forhånd (f.eks. antal poster, filer, IDs).
  • Hver iteration øger atomisk en DoneCounter.
  • En UI-timer læser tælleren periodisk og sætter ProgressBar.Position.

Fordel: Ingen TThread.Queue per element, ingen overbelastning af UI’en. Ulempe: Man har ikke detaljemeddelelser per element (f.eks. filnavn). Til det kan man tilføje en anden, begrænset statusmeddelelse (se næste afsnit).

Statusmeddelelser uden spam: „sidste status vinder“

Hvis der desuden skal vises en kort tekst (aktuelt element, fase, fejlmeddelelse), kræves et mønster, der ikke oversvømmer UI’en ved hvert worker-trin. I praksis fungerer „sidste status vinder“ meget godt:

  • Worker skriver en statusinformation til en trådsikker struktur (f.eks. atomisk udskiftelig string, eller beskyttet med en lille Lock).
  • UI-timeren overfører periodisk den senest sete status til et label.

Så forbliver UI’en reaktionsdygtig, og man ser stadig, at „noget sker“. Vigtigt her er mindre selve stringen end dens levetid: ingen referencer til kortlivede objekter fra worker-tråde må trækkes ind i UI-tråden. Hvis objekter overdrages, afklar ownership eksplicit.

TParallel.For trådsikker Progress-UI med TThread.Queue: et robust mønster

Der er scenarier, hvor en UI-timer alene ikke er tilstrækkelig: f.eks. når man til sidst ønsker at udløse præcis én „Fertig“-opdatering, eller når UI-opdateringen er et mere komplekst trin (f.eks. en post i et logvindue, men begrænset hyppighed). Så er TThread.Queue passende – men ikke per iteration, snarere selektivt.

En praktisk tilgang er en kø kun for begivenheder med lav frekvens:

  • Start-event (forberede UI, deaktivere knapper)
  • Periodiske Progress-Events (maks. hver X millisekunder)
  • Fehler-Events (valgfrit aggregeret)
  • Done-Event (nulstille UI, vise resultatet)

Periodikken opnås ikke via UI-tråden, men allerede i worker-konteksten: man lader kun workere queuee et UI-opdatering, når der er gået tilstrækkelig tid siden sidste UI-opdatering. Til det egner en monoton tidskilde sig (f.eks. TickCount) plus en atomar „last update“-værdi.

Vigtigt: UI-opdateringen selv skal være „hurtig“. Tunge beregninger, fil-I/O eller databaseadgang hører ikke hjemme i den gequeuede UI-callback. Callback’en bør kun læse tilstande og sætte Controls.

Cancel-Handling: Afbryd uden at få programmet til at hænge

I rigtige applikationer er afbryd ikke valgbar. Afgørende er: Cancel er ikke et „Kill“, men en kooperativ afslutning. Workere skal regelmæssigt kontrollere, om et afbrudssignal er sat, og derefter afslutte ordentligt. I Delphi findes der flere måder (afhængigt af PPL-konstruktionen): et eget Volatile-flag, en atomar Boolean, eller et Cancellation-koncept via Tasks (afhængigt af Delphi-version og struktur).

For driften er to regler vigtige:

  • Annuller skal blive synlig hurtigt: Kontroller annulleringsflaget ved fornuftige steder, ikke kun i slutningen af en iteration, hvis iterationen kan vare flere sekunder.
  • Annuller skal rydde op: Åbne Handles, midlertidige filer, transaktioner eller Locks må ikke blive efterladt. Det vil sige: I hver worker-iteration er try/finally-blokke obligatoriske, når ressourcer er involveret.

UI-mæssigt bør Annuller kun sætte et signal og bringe UI en i en „Stopping…“-tilstand. Den egentlige afslutning og genaktivering af UI sker derefter i Done-eventet, ikke straks ved klik.

Undgå deadlocks: de hyppigste faldgruber i praksis

Diagram af cyklisk venten mellem tråde som visualisering af et deadlock
Deadlocks opstår ofte ved cyklisk venten: UI-tråden blokeret, workerne venter på UI-adgang.

Fælde 1: WaitFor/Task.Wait i hovedtråden

Hvis hovedtråden blokeres, kan den ikke udføre Queue-callbacks og kan ikke behandle beskeder. Det ligner et deadlock, selvom workerne fortsætter korrekt. Løsning: Ingen blokerende waits i UI-tråden. I stedet afslutningshandling via TThread.Queue eller en hændelsesstyring (f.eks. en timer, der tjekker „færdig“).

Fælde 2: Synchronize inden for et Lock

En klassiker: Worker holder en Critical Section, kalder så Synchronize, og i UI-callbacket kræves (direkte eller indirekte) den samme Critical Section igen. Resultat: cirkulært venten. Reglen er simpel: Ingen overlevering til UI (Synchronize/Queue) fra et holdt Lock. Hvis et Lock er nødvendigt, hent alle data først til lokale variabler, frigiv Locket, og så queue’er du.

Fælde 3: UI-Callback udløser Reentrancy

Nogle gange er UI-opdateringen i sig selv ikke „harmløs“: At sætte Properties kan udløse Events (OnChange, OnResize), som igen starter logik, der tilgår worker-tilstande. Det er ikke et deadlock i snæver forstand, men fører til svært forklarlige frysninger og Race Conditions. Afhjælpning: Læg UI-opdateringer i „stille“ spor (deaktiver Events midlertidigt) eller brug Reentrancy-Guards (f.eks. en atomar guard for opdateringsfasen).

Fælde 4: For mange queued Updates

Selv uden Waits kan UI’en „stå stille“, hvis du producerer titusindvis af queued Callbacks. Symptomer: ProgressBar fortsætter længe, vinduet reagerer trægt, CPU i hovedtråden høj. Løsning: Drossel (tidsvindue), aggregér (counter), eller en ægte Producer/Consumer-struktur, hvor kun én UI-opdatering kan være „pending“ (Coalescing).

Når det bliver mere kompliceret: indsamle resultater, samle fejl, sikre rækkefølge

TParallel.For er ideel, når iterationerne er uafhængige. I forretningssoftware er iterationer dog ofte kun „tilnærmelsesvis“ uafhængige: De læser filer, kalder REST-APIs, skriver databaserækker. Så skal du planlægge tre yderligere punkter nøje:

  • Trådsikker resultatsamling: Enten per tråd en lokal buffer (sammenflet til sidst) eller en trådsikker Queue/Collection. Undgå Locks i Hot Path.
  • Fejlhåndtering: Exceptions fra worker-tråde skal indsamles. I praksis viser det sig nyttigt at huske den første Exception og udløse en annullering, eller at indsamle alle Exceptions og vise dem sammenfattet til sidst.
  • Rækkefølge: Hvis output kræver en stabil rækkefølge (f.eks. logs efter indeks), er parallel behandling med efterfølgende sortering ofte lettere end et »trådsikkert ordnet indsætnings«-mønster.

For UI betyder det: Vis ikke hver enkelt fejlmeddelelse med det samme. Det fører til helvede af modale dialoger. Indsaml fejl (f.eks. en liste af strenge), og vis til sidst et resumé eller en eksporterbar log.

Debugging: Hvordan du virkelig gør deadlocks synlige

Deadlocks i parallel kode er frustrerende, fordi de kan se anderledes ud i debuggeren end i release. Alligevel er der nogle praktiske greb:

Brug Threads-vinduet og call stacks

Hvis UI hænger, så inspicer alle tråde: Hvor står hovedtråden? Venter den? Er den i en message-loop? Hvor står worker-trådene? Hvis workers sidder fast i Synchronize, er årsagen næsten altid »hovedtråden blokeret« eller »hovedtråden mangler en lock«.

Marker Queue-/Synchronize-steder

Placer målrettet logging ved overleveringssteder (før Queue, i Queue-callback, ved slutningen af iterationen). I drift er det ofte mere værdifuldt end breakpoints, fordi timing er afgørende. Sørg for, at selve logging er trådsikker og ikke-blokerende (f.eks. ingen UI-logudskrifter direkte fra worker-tråde).

Mål tid for sidste opdatering

Når UI »hænger«, kan det ganske enkelt skyldes, at den skal behandle for mange opdateringer. Mål derfor i hovedtråden, hvor ofte du per sekund udfører et UI-opdatering, og hvor lang tid det tager. Så snart UI-callbacks tager mere end få millisekunder, er det nødvendigt at begrænse eller forenkle.

Hvornår er TParallel.For med Progress-UI virkelig værd at bruge?

Parallelisering er ikke et mål i sig selv. Den er særlig nyttig, når:

  • iterationerne er store nok (millisekunder til sekunder), så threadpool-overhead forsvinder,
  • jobbet er CPU-intensivt (parsing, komprimering, hashing) eller har godt paralleliserbar I/O (flere filer, flere HTTP-requests med begrænsninger),
  • du har en klar annullerings- og fejlstrategi,
  • UI-kravene kan nøjes med aggregeret fremdrift.

Den er mindre nyttig, når hver iteration er ekstremt kort (mikrooperationer), eller når alle iterationer rammer den samme flaskehals (en seriel DB-transaktion, en global lås, en enkelt fil). Så er et hurtigere greb ofte: forbedre algoritmen, batchbehandle, reducere dataadgang eller eksplicit afkoble flaskehalsen.

Praktisk tjekliste: Sådan forbliver UI stabil

  • Hovedtråden blokerer ikke: ingen waits, ingen lange løkker uden en message pump.
  • Worker må aldrig røre ved Controls: ingen VCL/FMX-adgang uden for UI-tråden.
  • UI-opdateringer er begrænset: Counter/Timer eller sammenlagte Queue-opdateringer i stedet for per iteration.
  • Ingen Synchronize-opkald indefra locks.
  • Annullering er kooperativ, kontrolleres hyppigt og rydder ordentligt op.
  • Exceptions indsamles og behandles ordnet til sidst.

Konklusion: Afkobling via Queue, stabilisering gennem aggregering

En robust progress-UI ved TParallel.For opstår ikke ved at „sætte Synchronize ind et eller andet sted“, men gennem et klart arkitekturprincip: workerne arbejder uafhængigt, maintråden forbliver fri og behandler kun få, hurtige UI-opdateringer. TThread.Queue er det rigtige værktøj, når du bruger det målrettet og begrænset. Ved „træg“ eller ustabil adfærd er det næsten altid to årsager: maintråden venter et sted blokerende – eller den drukner i for mange queued opdateringer.

Når du har sat mønsteret korrekt op (Counter/Coalescing, Cancel-Flag, Abschluss-Callback), lønner det sig på tværs af mange steder i en etableret Delphi-applikation: Import/Export, datavalideringer, fil- og API-jobs – alt bliver mere responsivt, uden at du for hver progress-opdatering risikerer nye Deadlocks.

Hvis du har brug for hjælp til at stabilisere parallelkode, til debugging af UI-hængere eller til en ordentlig modernisering af etablerede Delphi-applikationer: Kontakt os.

I forbindelse med dette emne er også Delphi Parallel Programming Library og Tthread.queue Vs Synchronize vigtige. Artiklen placerer disse aspekter forståeligt og viser, hvad der er vigtigt i praksis.

Drøft et projekt eller moderniseringsforløb med Net-Base.

Næste trin

Når emnet bliver til et reelt projekt, bør arkitektur, eksisterende systemer og drift tidligt vurderes samlet.

Vi støtter ikke kun ved enkeltspørsmål, men også når kildekodeudsnit, legacy-komponenter eller portalidéer skal udvikles til et robust virksomhedsprojekt.

  • Eksisterende tilstand, målbillede og tekniske risici vurderes samlet.
  • REST, dataadgang, portaler og udrulning bliver ikke udskudt som efterfølgende opgaver.
  • De ser tidligt, hvilken vej der er økonomisk og driftsmæssigt bæredygtig.

Del indlæg

Del dette indlæg direkte

LinkedIn, X, XING, Facebook, WhatsApp og e-mail er straks tilgængelige. Til Instagram forbereder vi link og kort tekst.

E-mail

Instagram åbner i en ny fane. Linket og kortteksten kopieres på forhånd til udklipsholderen.