Net-Base Magasin

24.07.2026

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

Slik kombinerer du Delphi TParallel.For med en trådsikker Progress-UI: oppdateringer via TThread.Queue, ryddig aggregering, avbruddshåndtering og typiske deadlock-feller ved feilsøking og i drift.

24.07.2026

Fra magasinetema til prosjektpraksis

Egnede tjeneste- og tekniske sider for innlegget

Den som i Delphi vil parallellisere beregningsintensive eller I/O-tunge jobber, ender raskt opp med Parallel Programming Library (PPL) og konkret med TParallel.For. Effekten merkes ofte umiddelbart – helt til man i samme åndedrag forsøker å oppdatere en Progress-UI «kjapt». Det er her de typiske hengene oppstår: tilsynelatende tilfeldige UI-freezes, en ProgressBar som hopper bakover, eller et komplett deadlock så snart man trinn-for-trinn feilsøker i debuggeren.

I dette innlegget handler det om TParallel.For trådsikker Progress-UI: et robust mønster som fungerer i VCL og FMX, som samler UI-oppdateringer ryddig via TThread.Queue, tar hensyn til Cancel/Abort og konsekvent unngår de vanligste deadlock-fallgrubene. Fokuset er på driftstilstand: repeterbart oppførsel, forståelige ansvarsfordelinger og feilsøkingstips som også hjelper når feilen kun opptrer «hos kunden».

Warum Progress-UI bei TParallel.For so oft schiefgeht

TParallel.For kjører typisk på worker-tråder fra Delphi-threadpoolen. Disse trådene må ikke berøre VCL- eller FMX-controls direkte, fordi UI-rammeverkene (meldingssløyfe, vindushåndtak, rendering) er bundet til Main Thread. Selv en tilsynelatende harmløs ProgressBar.Position := … fra en worker kan føre til udefinert oppførsel: sporadiske AVs, frosne vinduer eller «flimrende» oppdateringer.

Den opplagte reparasjonen er ofte TThread.Synchronize. Det løser riktignok trådsikkerheten, men fører i parallelle løkker raskt til et annet problem: det skaper en seriell flaskehals. Hver worker venter på Main Thread, som igjen er opptatt med rendering og med å behandle Synchronize-kall. Under belastning ser det ut som et deadlock – selv om det «bare» er en starvation/lockstep-effekt.

Og så har vi den ekte deadlock-klassen: Main Thread venter (f.eks. ved WaitFor, Task.Wait eller indirekte via blokkende kall) på slutten av den parallelle operasjonen, mens worker-tråder prøver å sende arbeid til Main Thread via Synchronize eller uheldig bruk av Queue. Resultat: Main Thread venter på worker, worker venter på Main Thread.

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

Unschärfer Debugger-Blick mit Notizen zu Threads und Queue als Kontext für Queue vs Synchronize
Under debugging ser man raskt om Worker-Threads venter på Main Thread eller bare legger ut queued oppdateringer.

Begge mekanismene sørger for å kjøre kode trygt i Main Thread. Forskjellen ligger i ventesemantikken:

  • TThread.Synchronize: Den kallende worker venter til Main Thread har utført koden. Det er «synkront», øker latenser og er en klassisk ingrediens i deadlocks når Main Thread er blokkert.
  • TThread.Queue: Worker legger koden bare i en kø for hovedtråden og fortsetter å kjøre. Det er «asynkront», frikobler tråder og er i parallellscenarier nesten alltid det bedre standardvalget – forutsatt at du kontrollerer oppdateringsfrekvensen.

Viktig: Queue er ingen fribillett. Hvis du i hver iterasjon av en løkke sender en Queue-oppdatering, oversvømmer du Main-Thread-Queue. Da henger ikke UI på grunn av deadlock, men på grunn av ren mengde meldinger. UI-en oppleves som treg, og ferdigbehandling forsinkes fordi hundrevis eller tusenvis av UI-oppdateringer fortsatt pågår.

Det randtilfellet som virkelig gjør vondt: Venting i UI-tråden

I virksomhetsapplikasjoner ser man ofte følgende forløp: Start-knappen klikkes, UI deaktiveres, ProgressDialog åpnes, og så «ventes» synkront til alt er ferdig før UI aktiveres igjen. Dette mønsteret er kjernen i mange deadlocks.

Typiske varianter (avhengig av kodebase):

  • Hovedtråden starter TParallel.For og kaller deretter en blokkerende ventelogikk (direkte eller indirekte).
  • En ProgressDialog kaller i konstruktøren eller OnShow en rutine som internt venter.
  • En Avbryt-knapp setter riktignok et flagg, men hovedtråden sitter likevel fast i en venteloop.

Hvis worker-tråder i denne perioden bruker Synchronize, er deadlock praktisk talt garantert. Med Queue kan det også henge dersom hovedtråden er blokkert og ikke pumper meldinger – for da blir heller ikke Queue behandlet.

Den driftsikre konsekvensen er: Hovedtråden må ikke blokkere og vente på parallellsløyfen hvis det samtidig kreves UI-oppdateringer. I stedet må behandlingen enten flyttes helt ut til en bakgrunnsoppgave, eller man organiserer en «asynkron avslutning» (callback/Queued avslutningshandling) som frigir UI når alt er ferdig.

Ryddig tilnærming: Progress kun aggregert, UI-oppdateringer throttles

Arbeidsplassmotiv med abstrakt fremstilling av fremdriftsindikator og tidtaking som symbol for throttlete UI-oppdateringer
Aggregering og tidsbasert throttling hindrer at UI blir oversvømmet av for mange oppdateringer.

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

  • Worker-Threads utfører selve arbeidet per element/index. De rapporterer kun fremdrift i en trådsikker form (teller, Queue, Thread-safe Queue).
  • Aggregator (ofte: hovedtråden eller en dedikert timer i UI) beregner fra fremdriften en UI-status (posisjon, tekst, ETA) og oppdaterer controls. Slik unngår du 1:1-oppdateringer per iterasjon.
  • Avslutning (også i hovedtråden): reaktivere UI, vise resultat, oppsummere feil, frigjøre ressurser.

Hvorfor denne separasjonen fungerer så godt: Workloaden kan være høyfrekvent (tusenvis av elementer), men UI trenger bare noen få oppdateringer per sekund. I praksis er 5–10 oppdateringer/sekund tilstrekkelig, ved svært raske jobber til og med 2–4. Alt over det er som regel bare visuell støy og koster CPU-tid i Message Pump.

Trådsikker telling: atomisk i stedet for Lock

For en enkel ProgressBar er ofte en atomær teller nok. «Atomær» betyr: inkrement og lesing skjer uten race condition, typisk via TInterlocked. Dermed unngår du Locks (Critical Sections) i løkkens hot path.

Anbefalt minimalidé:

  • Totalmengden er kjent på forhånd (f.eks. antall poster, filer, ID-er).
  • Hver iterasjon øker en DoneCounter atomisk.
  • En UI-timer leser telleren periodisk og setter ProgressBar.Position.

Fordel: Ikke TThread.Queue per element, ingen UI-overflod. Ulempe: Du får ingen detaljmeldinger per element (f.eks. filnavn). Til det kan man legge til en andre, dempet statusmelding (se neste avsnitt).

Statusmeldinger uten spam: „siste status vinner“

Hvis du i tillegg vil vise en kort tekst (nåværende element, fase, feilmelding), trengs et mønster som ikke flommer UI-et ved hvert worker-trinn. I praksis fungerer «siste status vinner» veldig godt:

  • Worker skriver en statusinformasjon til en trådsikker struktur (f.eks. atomært utbyttbar streng, eller beskyttet med en liten Lock).
  • UI-timer overtar periodisk den sist observerte statusen til en label.

Slik forblir UI responsiv, og du ser likevel at «noe skjer». Viktigere enn selve strengen er levetiden: ikke dra referanser til kortlivede objekter fra worker-tråder inn i UI-tråden. Hvis du overfører objekter, avklar eierskap eksplisitt.

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

Det finnes scenarier hvor en UI-timer alene ikke er nok: for eksempel når du på slutten vil sikre at nøyaktig ett «Fertig»-oppdatering utløses, eller når UI-oppdateringen er et mer komplekst steg (f.eks. innføring i et loggvindu, men dempet). Da er TThread.Queue passende – men ikke per iterasjon, i stedet målrettet.

En praksisnær tilnærming er en queue kun for hendelser med lav frekvens:

  • Start-hendelse (forberede UI, låse knapper)
  • Periodiske Progress-hendelser (maks. hvert X millisekund)
  • Feil-hendelser (valgfritt aggregerte)
  • Done-hendelse (tilbakestille UI, vise resultat)

Periodiciteten håndterer du ikke i UI-tråden, men allerede i worker-konteksten: du lar worker bare queue et UI-oppdatering når nok tid har gått siden forrige UI-oppdatering. Til dette egner en monoton tidskilde seg (f.eks. TickCount) pluss en atomær «last update»-verdi.

Viktig: UI-oppdateringen selv må være «rask». Kostbare beregninger, fil-I/O eller databaseaksesser hører ikke hjemme i den queued UI-callbacken. Callbacken bør kun lese ut tilstander og sette Controls.

Cancel-håndtering: Avbryt uten låsing

I ekte applikasjoner er avbryt ikke valgfritt. Avgjørende er: Cancel er ikke et «Kill», men en kooperativ avslutning. Workere må jevnlig sjekke om et avbruddsignal er satt, og så avslutte ryddig. I Delphi finnes det flere måter for dette (avhengig av PPL-konstruksjon): et eget Volatile-flagg, en atomær Boolean, eller et Cancellation-konsept over Tasks (avhengig av Delphi-versjon og struktur).

For driften er to regler viktige:

  • Cancel må bli synlig raskt: Sjekk avbruddsflagget på fornuftige steder, ikke bare på slutten av en iterasjon hvis iterasjonen kan ta flere sekunder.
  • Cancel må rydde opp: Åpne handles, midlertidige filer, transaksjoner eller låser må ikke bli liggende. Det betyr: I hver worker-iterasjon er try/finally-blokker obligatoriske når ressurser er involvert.

På UI-siden bør Cancel bare sette et signal og sette UI i en «Stopping…»-tilstand. Den faktiske avslutningen og reaktiveringen av UI skjer i Done-Eventet, ikke umiddelbart ved klikk.

Unngå deadlocks: de vanligste fellene i praksis

Diagramm eines zyklischen Wartens zwischen Threads als Visualisierung eines Deadlocks
Deadlocks oppstår ofte ved syklisk venting: UI-Thread blokkerer, Worker venter på UI-tilgang.

Felle 1: WaitFor/Task.Wait i Main Thread

Når Main Thread blokkerer, kan den ikke utføre Queue-callbacks eller behandle meldinger. Det oppfører seg som en deadlock, selv om workerne fortsetter korrekt. Løsning: Ingen blokkende waits i UI-Thread. I stedet fullføringshandling via TThread.Queue eller en hendelsesstyring (f.eks. timer som sjekker «ferdig»).

Felle 2: Synchronize innenfor en Lock

En klassiker: Worker holder en Critical Section, kaller så Synchronize, og i UI-callbacken trengs (direkte eller indirekte) den samme Critical Section igjen. Resultat: sirkulær venting. Reglene er enkle: Ingen UI-overføring (Synchronize/Queue) mens et holdt lock er aktivt. Hvis et lock er nødvendig, hent alle data først i lokale variabler, slipp locken, og så queueer du.

Felle 3: UI-Callback utløser Reentrancy

Noen ganger er selve UI-oppdateringen ikke «harmløs»: Å sette properties kan utløse hendelser (OnChange, OnResize), som igjen starter logikk som aksesserer worker-tilstander. Dette er ikke et deadlock i snever forstand, men fører til vanskelig forklarlige hengere og race conditions. Mottiltak: Legg UI-oppdateringer i «stille» baner (deaktiver hendelser midlertidig) eller bruk reentrancy-guards (f.eks. en atomisk guard for oppdateringsfasen).

Felle 4: For mange queued oppdateringer

Selv uten waits kan UI-en «stoppe» hvis du genererer titusenvis av queued callbacks. Symptomer: ProgressBar løper lenge etter, vinduet reagerer tregt, CPU-bruk i Main Thread høy. Løsning: Demp (tidsvindu), aggregere (teller), eller en ekte producer/consumer-struktur hvor kun én UI-oppdatering kan være «pending» (coalescing).

Når det blir mer komplisert: samle resultater, samle feil, garantere rekkefølge

TParallel.For er ideelt når iterasjoner er uavhengige. I forretningsprogramvare er iterasjoner ofte bare «tilnærmet» uavhengige: Du leser filer, kaller REST-APIer, skriver databasrader. Da må du planlegge tre ekstra punkter nøye:

  • Trådsikker resultatinnsamling: Enten per tråd en lokal buffer (sammenfør til slutt) eller en trådsikker kø/Collection. Unngå låser i hot path.
  • Feilhåndtering: Exceptions fra worker-tråder må samles inn. I praksis fungerer det godt å enten huske den første Exception og utløse Cancel, eller samle alle Exceptions og vise dem samlet til slutt.
  • Rekkefølge: Hvis utdataene trenger en stabil rekkefølge (f.eks. logger etter indeks), er parallell behandling med etterfølgende sortering ofte enklere enn «trådsikker ordnet innsatt».

For UI betyr dette: Vis ikke hver enkelt feilmelding umiddelbart. Det fører til en flom av modal-dialoger. Samle feil (f.eks. en liste med strenger) og vis til slutt en oppsummering eller en eksportbar logg.

Debugging: Hvordan du virkelig gjør deadlock synlig

Deadlocks i parallellkode er frustrerende fordi de kan se annerledes ut i debuggeren enn i Release. Likevel finnes det noen praktiske grep:

Bruk Threads-vinduet og kallstakker

Når UI henger, undersøk alle trådene: Hvor står hovedtråden (Main Thread)? Venter den? Er den i en meldingssløyfe? Hvor står worker-trådene? Hvis workere sitter i Synchronize, er årsaken nesten alltid «hovedtråden blokkerer» eller «hovedtråden trenger en lås».

Marker kø-/Synchronize-steder

Plasser målrettet logging ved overleveringspunkter (før Queue, i Queue-callback, ved slutten av iterasjonen). I drift er dette ofte nyttigere enn breakpoints, fordi timing er avgjørende. Sørg for at selve loggingen er trådsikker og ikke-blokkerende (f.eks. ingen UI-loggutsendelse direkte fra worker-tråder).

Mål siste-oppdateringstid

Hvis UI «henger», kan det enkelt være fordi den må behandle for mange oppdateringer. Mål derfor i hovedtråden hvor ofte du per sekund utfører et UI-oppdatering og hvor lang tid det tar. Så snart UI-callbacks bruker mer enn noen få millisekunder, er det nødvendig med drossling eller forenkling.

Når lønner TParallel.For med Progress-UI seg virkelig?

Parallellisering er ikke et mål i seg selv. Den lønner seg særlig når:

  • iterasjonene er store nok (millisekunder til sekunder), slik at threadpool-overhead forsvinner,
  • jobben er CPU-intensiv (parsing, komprimering, hashing) eller har godt paralleliserbar I/O (flere filer, flere HTTP-forespørsler med begrensninger),
  • du har en klar Cancel- og feilstrategi,
  • UI-kravene kan leve med aggregert fremdrift.

Den lønner seg mindre når hver iterasjon er ekstremt kort (mikrooperasjoner) eller når alle iterasjoner rammer samme flaskehals (en seriell DB-transaksjon, en global lås, en enkelt fil). Da er ofte raskere tiltak: forbedre algoritmen, batchbehandling, redusere dataadgang eller eksplisitt løsne flaskehalsen.

Praksis-sjekkliste: Slik holder du UI stabil

  • Hovedtråden blokkerer ikke: ingen waits, ingen lange løkker uten meldingspump.
  • Workere berører aldri kontroller: ingen VCL/FMX-tilgang utenfor UI-tråden.
  • UI-oppdateringer er drosslet: tellere/timere eller samlede kø-oppdateringer i stedet for per iterasjon.
  • Ingen Synchronize-kall fra innenfor låser.
  • Cancel er kooperativ, sjekkes ofte og rydder opp korrekt.
  • Exceptions samles og behandles ordnet til slutt.

Konklusjon: Løsne med kø, stabiliser med aggregering

En robust Progress‑UI ved TParallel.For oppstår ikke ved å «bare putte Synchronize et sted», men ved et klart arkitekturprinsipp: workerne jobber uavhengig, hovedtråden holdes fri og behandler kun noen få, raske UI‑oppdateringer. TThread.Queue er det riktige verktøyet når du bruker det målrettet og med begrensning. For «treig» eller ustabil oppførsel er det nesten alltid to årsaker: hovedtråden venter et sted blokkerende – eller den drukner i for mange queued oppdateringer.

Når du har satt opp mønsteret rent (Counter/Coalescing, Cancel-Flag, Abschluss-Callback), lønner det seg på mange steder i en etablert Delphi‑applikasjon: Import/Export, datavalideringer, fil‑ og API‑jobber – alt blir mer responsivt, uten at du pådrar deg nye deadlocks ved hver fremdriftsoppdatering.

Hvis du trenger støtte til å stabilisere parallelkode, til feilsøking av UI‑hengere eller til en ryddig modernisering av etablerte Delphi‑applikasjoner: ta kontakt.

For dette temaet er også Delphi Parallel Programming Library og Tthread.queue Vs Synchronize viktige. Innlegget setter disse aspektene i forståelig kontekst og viser hva som betyr noe i det daglige.

Diskuter prosjekt eller moderniseringsprosjekt med Net-Base.

Neste trinn

Når et tema blir et reelt prosjekt, bør arkitektur, eksisterende systemer og drift vurderes samlet allerede tidlig i prosessen.

Vi bistår ikke bare med enkeltspørsmål, men også når kodesnutter, legacy-temaer eller portalideer skal utvikles til et robust virksomhetsprosjekt.

  • Eksisterende tilstand, målbildet og tekniske risikoer vurderes samlet.
  • REST, datatilgang, portaler og utrulling blir ikke utsatt som etterfølgende oppgaver.
  • Dere ser tidlig hvilken vei som er økonomisk og driftsmessig levedyktig.

Del innlegg

Del dette innlegget direkte

LinkedIn, X, XING, Facebook, WhatsApp og e-post er umiddelbart tilgjengelige. For Instagram forbereder vi lenke og kort tekst umiddelbart.

E-post

Instagram åpnes i en ny fane. Lenken og kortteksten kopieres først til utklippstavlen.