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 viaSynchronize. Båda väntar – slut. - Lock-Inversion: Workern håller ett lås (t.ex.
TCriticalSectionellerTMonitor) och anroparSynchronize. 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änderSynchronize. - Signalera trådavslut via Event/Callback: UI förblir responsiv, städar först upp efter signal.
- UI-uppdateringar postas i regel via
TThread.Queueeller 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
Synchronizedirekt 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.
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 avRaisedObjefterWaitForkonsistent. Trots det börRaisedObjvara lokal per anrop (som här), aldrig global.
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.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:
- Shutdown-Flag setzen (t.ex.
TUiDispatcher.BeginShutdown): Från och med nu inga nya UI-jobb. - Worker kooperativ stoppen: Workern kontrollerar en cancel-flagga (t.ex.
TEventeller liknandeTCancellationToken) och avslutar loopar/ väntan. - 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).
- 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,Sleepin 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:
- Worker kör Query/REST-anrop, beräknar resultat och skapar DTO.
- Worker postar DTO via
Queue/TUiDispatcher.Posttill UI. - 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.