Net-Base Magasin

15.05.2026

TThread och Synchronize utan UI-deadlocks: robusta mönster för VCL och legacykod

Hur du arbetar pålitligt med TThread, Synchronize och Queue utan att UI:n fryser: vanliga orsaker till deadlocks, ett praktiskt UI-dispatcher-mönster (inkl. Timeout), skydd vid nedstängning, låsstrategier och felsökningskontroller för väletablerade Delphi-applikationer.

15.05.2026

Den som i Delphi arbetar med trådar hamnar förr eller senare vid TThread.Synchronize. Och just där uppstår de obehagliga problemen: sporadiska frysningar, „UI reagerar inte“, till synes slumpmässiga deadlocks vid avslut eller när en dialog öppnas. Kärnan är sällan „Delphi är trasig“, utan nästan alltid en olycklig mix av Synchronize, blockerande väntoperationer och en UI-tråd som inte längre korrekt bearbetar sin Message Loop (VCL:ens händelsehantering). Detta inlägg visar robusta, i ett legacy-kontekst praktiskt gångbara mönster för TThread och Synchronize utan UI-deadlocks – inklusive timeout-variant, ren felvidarebefordran, shutdown-regler och debugging-tips som hjälper i verkliga beståndsapplikationer.

Varför deadlocks kring Synchronize uppstår i praktiken

Synchronize innebär: en worker-tråd lägger upp en procedur i en kö som i huvudtråden körs, och väntar typiskt tills den proceduren är klar. I VCL-applikationer är huvudtråden samtidigt UI-tråden (fönster, kontroller, händelser). Dessutom körs i många installationer där COM-objekt i STA-modellen (Single-Threaded Apartment: COM-anrop måste hanteras i samma tråd), vilket förstärker beroendet av en fungerande Message Loop.

Deadlocks uppstår vanligtvis genom någon av följande konstellationer:

  • WaitFor i huvudtråden: UI-tråden väntar på en worker (t.ex. MyThread.WaitFor), medan workern just behöver UI-tråden via Synchronize. Båda väntar – slut.
  • Lock-Inversion: Workern håller ett lås (t.ex. TCriticalSection eller TMonitor) och anropar Synchronize. Den synkroniserade UI-proceduren försöker ta samma lås (direkt eller indirekt, ofta via logging/cache/singletons) – klassisk deadlock.
  • Shutdown/Destroy: När en form stängs avslutas en tråd medan Synchronize-uppgifter fortfarande väntar. Särskilt illa: synkroniserade anrop refererar till kontroller som just håller på att förstöras.
  • Message Loop blockeras: Modala dialoger, långkörande UI-operationer, ett blockerande COM-anrop eller en handler som „snabbt“ gör DB/REST håller kvar huvudtråden. Synchronize-uppgifter körs försenat eller inte alls.

Den viktigaste konsekvensen för arkitektur och drift: Synchronize är en blockeringskant. I skräddarsydd företagsmjukvara med importer, BDE-ersättning med native anslutning-Queries, integrationsjobb eller bakgrundstjänster med UI-komponent bör denna kant kontrolleras medvetet – annars blir „sällan“ så småningom „alltid när det är bråttom“.

Grundregel: UI-tråden får aldrig vänta på workern (när Synchronize är inblandat)

Om en worker någonstans använder Synchronize bör huvudtråden inte hårt blockera och vänta på den workern. Det låter trivialt, men i legacy-kod är detta en av de vanligaste orsakerna, eftersom „vi väntar snabbt vid stängning“ eller „progress-dialog väntar på slut“ snabbt byggs in.

Praktiska konsekvenser:

  • Inga WaitFor-anrop på UI-tråden när det i workern finns en väg som använder Synchronize.
  • Signalera trådavslut via Event/Callback: UI förblir responsiv, städar först upp efter signal.
  • UI-uppdateringar postas i regel via TThread.Queue eller en dispatcher så att workern inte blockeras.

TThread.Queue är ofta det bättre standardvalet: Workern postar arbete till huvudtråden, fortsätter köra och blockeras inte. Det förhindrar många deadlocks. Det löser dock inte alla randfall – till exempel om du i en worker absolut behöver ett resultat som skapas i huvudtråden (t.ex. åtkomst till en UI-bunden resurs eller en komponent som är trådberoende).

TThread och Synchronize utan UI-deadlocks: tankemodell för rena överlämningar

En robust tankemodell är: Det finns endast få legitimt synkrona överlämningar till huvudtråden. Allt annat är status, presentation eller telemetri – och därmed asynkront.

En enkel indelning hjälper vid granskningar och vid stabilisering av befintliga projekt:

  • „Endast visa“: Förloppsindikator, loggrad, räknare, trafikljus, aktivera/inaktivera – alltid Queue.
  • „Överför tillstånd“: Workern levererar dataobjekt/DTO, UI renderar – Queue, men med kopiering/oföränderlighet (dvs inga gemensamt muterade strukturer).
  • „UI måste avgöra“: Bara här behöver du synkron semantik (t.ex. användarfråga). Då är den egentliga frågan: måste verkligen en worker vänta, eller kan arbetsflödet byggas om (state machine, avbryt jobb, fortsätt senare)?

Just den tredje kategorin är en deadlock-fälla: Om workern väntar på ett UI-resultat blir UI ofta frestad att vänta på workern (eller indirekt via lås). Det inträffar avsevärt oftare under hög belastning, vid långsamma databaser eller i Remote-Desktop-miljöer.

Källkodssnutt: UI-dispatcher med Queue, valbar timeout och ordnad avstängning

Mönstret nedan kapslar in UI-överföringar i en liten hjälparklass. Du får:

  • Post: Fire-and-forget via TThread.Queue (typiskt för statusuppdateringar).
  • Call: Synkront anrop med Timeout (ovanligt, men användbart i legacy-situationer), utan att använda Synchronize direkt som blockeringspunkt.
  • Shutdown-skydd: Ta inte emot nya UI-jobb, och köade jobb kontrollerar en flagga innan kontroller manipuleras.

Teknisk klassificering: Vi använder Queue plus TEvent (en Kernel-Event) för återkoppling. Workern väntar inte på Synchronize, utan på ett event som sätts i huvudtråden efter att den köade åtgärden har körts. Timeouten förhindrar „evigt“ hängande om UI-tråden av någon anledning inte längre bearbetar.

Delphi
unit UiDispatch;

interface

uses
  System.SysUtils,
  System.Classes,
  System.SyncObjs;

type
  EUiDispatchTimeout = class(Exception);
  EUiDispatchShuttingDown = class(Exception);

  /// <summary>
  ///  Kapselt UI-Aufrufe aus Worker-Threads.
  ///  Post: asynchron (Queue).
  ///  Call: synchron mit Timeout, ohne TThread.Synchronize direkt zu blocken.
  /// </summary>
  TUiDispatcher = class
  strict private
    class var FShuttingDown: Integer;
  public
    class procedure BeginShutdown; static;
    class function IsShuttingDown: Boolean; static;

    class procedure Post(const AProc: TProc); static;
    class procedure Call(const AProc: TProc; ATimeoutMs: Cardinal = 5000); static;
  end;

implementation

{ TUiDispatcher }

class procedure TUiDispatcher.BeginShutdown;
begin
  TInterlocked.Exchange(FShuttingDown, 1);
end;

class function TUiDispatcher.IsShuttingDown: Boolean;
begin
  Result := TInterlocked.CompareExchange(FShuttingDown, 0, 0) = 1;
end;

class procedure TUiDispatcher.Post(const AProc: TProc);
begin
  if not Assigned(AProc) then
    Exit;

  // Im Shutdown keine neuen UI-Jobs mehr annehmen.
  if IsShuttingDown then
    Exit;

  // Queue blockiert den Worker nicht.
  TThread.Queue(nil,
    procedure
    begin
      if IsShuttingDown then
        Exit;
      AProc();
    end);
end;

class procedure TUiDispatcher.Call(const AProc: TProc; ATimeoutMs: Cardinal);
var
  DoneEvent: TEvent;
  RaisedObj: TObject;
begin
  if not Assigned(AProc) then
    Exit;

  if IsShuttingDown then
    raise EUiDispatchShuttingDown.Create('UI-Dispatcher ist im Shutdown.');

  DoneEvent := TEvent.Create(nil, True, False, '');
  try
    RaisedObj := nil;

    TThread.Queue(nil,
      procedure
      begin
        try
          if not IsShuttingDown then
            AProc();
        except
          // Exception-Objekt über die Thread-Grenze reichen.
          // Achtung: Kein "raise" hier, sonst landet es im Main Thread.
          RaisedObj := AcquireExceptionObject;
        end;
        DoneEvent.SetEvent;
      end);

    case DoneEvent.WaitFor(ATimeoutMs) of
      wrSignaled:
        begin
          if Assigned(RaisedObj) then
            raise Exception(RaisedObj);
        end;
      wrTimeout:
        raise EUiDispatchTimeout.CreateFmt(
          'Timeout nach %d ms: Main Thread hat UI-Aufruf nicht abgearbeitet.',
          [ATimeoutMs]);
    else
      raise Exception.Create('Unerwarteter WaitFor-Status im UI-Dispatcher.');
    end;
  finally
    DoneEvent.Free;
  end;
end;

end.

Syftet med koden och var den medvetet ”ovanlig” är

Mönstret ersätter Synchronize inte helt, men det gör synkrona överlämningar kontrollerbara: Worker väntar inte på Synchronize‑mekanismen utan på ett Event. På så sätt kan ni tvinga fram timeouter, göra det synligt i drift att UI‑tråden hänger, och i en shutdown‑fas konsekvent avvisa nya UI‑jobb.

Den ”ovanliga” delen är inte Eventet, utan beslutet att återskapa synkron semantik med Queue + Event. Det är motiverat precis när ni i befintliga applikationer behöver stegvis eftermontera stabilitet utan att omedelbart göra arkitektoniska ändringar i varje Synchronize-ställe.

Randvillkor och fallgropar

  • Minnessynlighet: DoneEvent är synkroniseringsgränsen. Därigenom är läsningen av RaisedObj efter WaitFor konsistent. Trots det bör RaisedObj vara lokal per anrop (som här), aldrig global.
  • Exception-Handling: AcquireExceptionObject förhindrar att undantaget i huvudtråden ”försvinner”. När det åter kastas i workern är stacktracen inte identisk med ursprunget, men felmeddelandet finns kvar i worker-loggen, och jobbet kan misslyckas på ett kontrollerat sätt.
  • Timeout ist Diagnose und Schutz: Den „reparerar“ inte en blockerad huvudtråd. Den förhindrar dock att workern binder resurser obegränsat (t.ex. BDE-Ablosung mit nativer Anbindung-transaktioner hålls öppna), och den gör felklassen mätbar.
  • Shutdown muss früh beginnen: BeginShutdown bör ingå i en central shutdown-sekvens (t.ex. mycket tidigt i OnCloseQuery för huvudformen). Annars kan UI-jobb fortfarande köas medan fönster redan har förstörts.
  • Lock-Strategie: so vermeiden Sie Lock-Inversionen mit UI-Callbacks

    Många deadlocks uppstår inte genom WaitFor, utan genom en oklar låsordning. Typiskt förlopp: workern låser ”datanmodell”, anropar UI-uppdatering via Synchronize, och UI-uppdateringen går åter in i ”datanmodell”. Det är logiskt begripligt men tekniskt förödande.

    Praktiska regler som fungerar i team:

    • Keine Locks über Thread-Grenzen halten: Innan en worker köar eller synkroniserar något mot UI ska funktionella lås vara frigjorda.
    • UI liest Snapshots: UI-callbacks bör inte titta ”live” i worker-strukturer utan visa kopior/snapshots (t.ex. DTO, Record, enkla värden).
    • Logging ist ein Lock-Kandidat: Om loggning internt använder en kö, fil-lås eller ett singleton kan det bli del av en deadlock. UI-callbacks bör hålla loggningen minimal eller skriva via en separat, icke-blockerande logg-pipeline.

    Om ni redan har en Layer-3-arkitektur (UI, Services/Domäne, infrastruktur som dataåtkomst): UI-callbacks bör helst bara göra UI. Allt som är ”Service” hör inte hemma i callbacken. Det minskar reentrancy-effekter avsevärt.

    Shutdown ohne Hänger: „nicht WaitFor, sondern kooperatives Stoppen“

    Vid avslut händer det ofta: UI stängs, en tråd ska tas bort, men köade UI-jobb är fortfarande öppna. Ett rent shutdown är mindre ”att döda en tråd” och mer en liten koreografi:

    1. Shutdown-Flag setzen (t.ex. TUiDispatcher.BeginShutdown): Från och med nu inga nya UI-jobb.
    2. Worker kooperativ stoppen: Workern kontrollerar en cancel-flagga (t.ex. TEvent eller liknande TCancellationToken) och avslutar loopar/ väntan.
    3. UI nicht blockieren: Ingen hård vänteslinga i huvudtråden. Om ni ”måste vänta” så endast med fortsatt meddelandeloop (eller ännu bättre: undvik helt genom att hantera avslut via callback).
    4. Letzte UI-Aufräumarbeiten endast om fönster/controls garanterat fortfarande existerar. I VCL är tidpunkten viktig: senast när handtaget är borta får köade jobb inte längre gå till controls.

    Denna procedur är relevant för drift och support: ”Applikationen hänger vid stängning” är ett klassiskt acceptansproblem, även om allt funktionellt har behandlats korrekt. En definierad shutdown sparar verklig tid här.

    Debugging: Wie Sie den Deadlock greifbar machen (ohne Rätselraten)

    När det hänger är kärnfrågan: Vem väntar på vem? Några angreppssätt som visat sig fungera i befintliga projekt:

    • Inventera alla väntpunkter: Volltextsuche nach WaitFor, Sleep in Schleifen, TEvent.WaitFor, INFINITE. Viele Probleme sind „versteckte“ Waits (auch in Bibliotheken).
    • Trådstatus i loggen: Logga vid trådgränser: „Jobb startar“, „köad UI“, „UI körs“, „Jobb klart“. På så vis ser ni om huvudtråden överhuvudtaget bearbetar köade jobb.
    • Kontrollera misstanke om Message Loop: Uppstår hang endast vid modala dialoger eller vissa COM-interaktioner är Message Loop ofta flaskhalsen. Målet då är: avlasta UI-handlers, isolera COM-anrop och undvika långa operationer i UI.
    • Gör lås synliga: För TCriticalSection/TMonitor är en debug-build med „Owner“-metadata (t.ex. tråd-ID vid Enter) och tidsmätning värdefull. Då ser ni vilket lås huvudtråden håller medan Worker väntar på UI.

    Viktig är inställningen: Deadlocks är sällan „slumpmässiga“. De är deterministiska cykler som bara sällan utlöses. När ni väl identifierat cykeln tydligt är åtgärden oftast klar.

    Varianter för dataåtkomst och gränssnittsjobb (FireDAC, REST, filsystem)

    Särskilt vid FireDAC (eller andra DB-åtkomster) gäller: anslutning, transaktion och datasets är i praktiken trådbundna. En Worker-tråd bör äga sin DB-kontext exklusivt. UI-anrop bör begränsa sig till presentation, inte DB-operationer. Ett robust mönster är:

    1. Worker kör Query/REST-anrop, beräknar resultat och skapar DTO.
    2. Worker postar DTO via Queue/TUiDispatcher.Post till UI.
    3. UI tar emot DTO och uppdaterar kontroller (utan återkoppling till Worker-objekt).

    Om ni har historiskt uppkomna hybridformer („UI triggar DB, DB-callback triggar UI“) är en stegvis avkoppling lämplig: först isolera överlämningspunkter (Dispatcher), sedan flytta tillstånd till Services/Model. Det är mindre riskfyllt än en stor omläggning, men minskar deadlocks märkbart.

    Slutsats: Att undvika deadlocks innebär att kontrollera överlämningar

    TThread und Synchronize ohne UI-Deadlocks är mindre en enstaka teknik än en disciplin: minimera blockeringar, håll låssekvenser ordnade, definiera Shutdown och reducera synkrona UI-beroenden. Den visade UI-dispatchern är i legacy-situationer särskilt användbar eftersom den använder Queue som standard, men för nödvändiga synkrona överlämningar lägger den till Timeout och tydliga Shutdown-regler.

    Begränsningar kvarstår: Om huvudtråden är permanent blockerad (på grund av tung UI-logik, modala dialogkedjor eller COM-STA-anrop) kan även en dispatcher endast diagnostisera och avbryta kontrollerat. Den hållbara lösningen är då att avlasta UI och separera ansvarsområden. Om ni behöver stöd för detta i en befintlig Delphi-applikation – från threading-fällor till stegvis stabilisering – kan ni placera ärendet här: Projekt eller Modernisierungsvorhaben mit Net-Base besprechen.

    I det tekniska sammanhanget spelar även Delphi multithreading och Synchronize-deadlock en viktig roll när integrationer, dataflöden och vidareutveckling måste samspela tydligt.

    Diskutera projekt eller moderniseringsprojekt med Net-Base.

    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.