Sá sem vinnur með þræði í Delphi lendir fyrr eða síðar á TThread.Synchronize. Og einmitt þar gerast óskemmtilegu hlutirnir: sporadískar kyrrstæður, „UI bregst ekki við“, og að því virðist tilviljanakenndir deadlocks við lokun eða við opnun samtalsglugga. Kjarninn er sjaldan „Delphi er bilað“, heldur nánast alltaf óheppileg blanda af Synchronize, blokkerandi biðaðgerðum og UI‑þræði sem vinnur ekki hreint upp sína Message Loop (viðburðarmeðferð VCL). Þessi grein sýnir traustar, í legacy-samhenginu hagnýtar sniðmát fyrir TThread og Synchronize án UI-deadlocks – þar með talin útgáfa með timeout, hreina villuviðameðferð, reglur fyrir lokun og leiðbeiningar við villuleit sem nýtast í raunverulegum eldri forritum.
Af hverju deadlocks kringum Synchronize myndast í raunverulegri notkun
Synchronize þýðir: Vinnuþráður setur aðgerð í biðröð sem er keyrð í Main Thread og bíðst venjulega eftir því að sú aðgerð ljúki. Í VCL-forritum er Main Thread jafnan UI-þráðurinn (gluggar, Controls, atburðarmeðhöndlun). Auk þess keyra í mörgum uppsetningum COM-þættir í STA-Modell (Single-Threaded Apartment: COM-köll verða að vera unnin í sama þræði), sem eykur háðina á virku Message Loop enn frekar.
Deadlocks myndast venjulega vegna einnar af eftirfarandi uppstillingum:
- WaitFor im Main Thread: UI-þráðurinn bíður eftir vinnuþræði (t.d.
MyThread.WaitFor), á meðan vinnuþráðurinn þarfnast núnaSynchronizetil að ná fram aðgerð í UI-þræðinum. Báðir bíða – endir. - Lock-Inversion: Vinnandi þráður heldur aðgangslás (t.d.
TCriticalSectioneðaTMonitor) og kallarSynchronize. Samstillta UI-aðferðin reynir að taka sama lás (beint eða óbeint, oft í gegnum logging/cache/singletons) – klassískur deadlock. - Shutdown/Destroy: Við lokun forms er þráður hættur meðan
Synchronize-verkefni eru enn í bið. Sérstaklega dæmalaust: samstillt köll vísa í Controls sem eru að vera eytt. - Message Loop blockiert: Modalir gluggar, langvarandi UI-aðgerðir, eitt blokkerandi COM-kall eða handler sem „bara svona“ framkvæmir DB/REST halda Main Thread föstum.
Synchronize-verkefni eru unnin seint eða alls ekki.
Meginafleiðingin fyrir arkitektúr og rekstur: Synchronize er blokkunarbrún. Í sérsniðnum fyrirtækjaforritum með innflutningum, BDE-Ablosung með natívri tengingu-fyrirspurnum, samskiptajobbum eða bakgrunnsþjónum með UI-íhluta ætti þessi jaðar að vera meðvitað stýrt – annars fer „sjaldan“ að verða „ávallt þegar á þarf að halda“.
Grundregel: UI-Thread nie auf Worker warten lassen (wenn Synchronize im Spiel ist)
Ef vinnuþráður notar einhvers staðar Synchronize, ætti Main Thread ekki að bíða hart og blokkerandi eftir þeim vinnuþræði. Þetta hljómar augljóst, en í legacy-kóða er þetta ein algengasta orsökin, þar sem „bíðum aðeins við lokun“ eða „framvindugluggi bíður eftir enda“ er fljótt bætt inn.
Hagnýtar afleiðingar:
- Keine
WaitFor-Aufrufe im UI-Thread, sobald im Worker ein Pfad existiert, derSynchronizenutzt. - Thread-Abschluss per Event/Callback signalisieren: UI bleibt responsiv, räumt erst nach Signal auf.
- UI-Updates grundsätzlich über
TThread.Queueoder einen Dispatcher posten, damit Worker nicht blockieren.
TThread.Queue ist häufig die bessere Default-Option: Der Worker postet Arbeit an den Main Thread, läuft weiter und blockiert nicht. Das verhindert viele Deadlocks. Es löst aber nicht alle Randfälle – etwa wenn Sie in einem Worker zwingend ein Ergebnis benötigen, das im Main Thread erzeugt wird (z. B. Zugriff auf eine UI-gebundene Ressource oder eine Komponente, die threadgebunden ist).
TThread und Synchronize ohne UI-Deadlocks: Denkmodell für saubere Übergaben
Ein belastbares Denkmodell ist: Es gibt nur wenige legitim synchrone Übergaben in den Main Thread. Alles andere ist Status, Darstellung oder Telemetrie – und damit asynchron.
Eine einfache Einteilung hilft in Reviews und bei der Stabilisierung von Bestandsprojekten:
- „Nur anzeigen“: Progress, Logzeile, Zähler, Ampel, Enable/Disable – immer
Queue. - „Zustand übergeben“: Worker liefert Datenobjekt/DTO, UI rendert –
Queue, aber mit Copy/Immutability (also keine gemeinsam mutierten Strukturen). - „UI muss entscheiden“: Nur hier brauchen Sie synchrone Semantik (z. B. Benutzerabfrage). Dann ist die eigentliche Frage: Muss wirklich ein Worker warten, oder kann der Workflow umgebaut werden (State Machine, Job abbrechen, später fortsetzen)?
Gerade die dritte Kategorie ist eine Deadlock-Falle: Wenn der Worker auf ein UI-Ergebnis wartet, wird die UI schnell dazu verleitet, auf den Worker zu warten (oder indirekt über Locks). Das kippt unter Last, bei langsamen Datenbanken oder bei Remote-Desktop-Umgebungen deutlich eher.
Source-Schnipsel: UI-Dispatcher mit Queue, optionalem Timeout und sauberem Shutdown
Das folgende Muster kapselt UI-Übergaben in eine kleine Hilfsklasse. Sie bekommen:
- Post: Fire-and-forget über
TThread.Queue(typisch für Statusupdates). - Call: Synchronous Call mit Timeout (ungewöhnlich, aber in Legacy-Situationen hilfreich), ohne direkt
Synchronizeals Blockadepunkt zu verwenden. - Shutdown-Schutz: Keine neuen UI-Jobs mehr annehmen, und queued Jobs prüfen einen Flag, bevor Controls angefasst werden.
Technische Einordnung: Wir nutzen Queue plus TEvent (ein Kernel-Event) zur Rückmeldung. Der Worker wartet nicht auf Synchronize, sondern auf ein Event, das im Main Thread gesetzt wird, nachdem die queued Action ausgeführt wurde. Das Timeout verhindert „ewiges“ Hängen, wenn der UI-Thread aus irgendeinem Grund nicht mehr zum Abarbeiten kommt.
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.Tilgangur kóðans og hvar hann er meðvitað „óvenjulegur“
Mynstrið kemur ekki alveg í stað Synchronize, en það gerir samstilltar yfirfærslur stjórnlegar: vinnuþráðurinn bíður ekki á Synchronize-meginrömmunni heldur eftir event. Með þessu er hægt að neyða fram timeout, gera í rekstri greint þegar UI-þráðurinn hangir, og í lokunarfasa hafna kerfislega nýjum UI-verkum.
Óvenjulega þátturinn er ekki eventið sjálft heldur ákvörðunin um að tákna samstillta rökfræði með Queue + Event. Þetta er sérstaklega gagnlegt þegar þarf að bæta stöðugleika smám saman inn í eldri kerfi án þess að þurfa að endurskipuleggja hverja Synchronize-staðsetningu strax í arkitektúrnum.
Randbedingungen und Stolperfallen
- Speichersichtbarkeit:
DoneEventer samstillingarmörk. Þess vegna er lestur áRaisedObjeftirWaitForsamkvæmur. Samt ættiRaisedObjað vera staðbundið fyrir hvern kall (eins og hér), aldrei globalt.
AcquireExceptionObject kemur í veg fyrir að undantekningin hverfi í aðalþræðinum. Þegar henni er varpað upp aftur í Worker er stacktrace ekki eins og upprunið, en villuboðin haldast í Worker-logginu og jobbið getur fallið hreint.BeginShutdown á heima í miðlægri lokunarröð (t.d. mjög snemma í OnCloseQuery aðalformsins). Annars verða enn UI-Jobs raðaðir á meðan gluggar eru þegar eyðilagðir.Lásastefna: hvernig forðast árekstra í lásaröð (Lock-Inversion) með UI-Callbacks
Mörg deadlocks eiga uppruna sinn ekki í WaitFor, heldur í óljósri röð læsinga. Dæmigerður gangur: Worker læsir „gagnamódelinu“, kallar á UI-uppfærslu með Synchronize, og UI-uppfærslan segir aftur aðgang að „gagnamódelinu“. Þetta er rökrétt framsetning en tæknilega skaðlegt.
Hagnýtar reglur sem auðvelt er að innleiða í teymum:
- Ekki halda læsingum yfir þræðamörkum: Áður en Worker raðar/samstillir neinu í átt að UI ættu faglegar læsingar að vera leystar.
- UI les afrit (Snapshots): UI-Callbacks ættu ekki að skoða „live“ Worker-strúktúrar, heldur sýna afrit/snapshots (t.d. DTO, Record, einföld gildi).
- Logging getur orðið hluti af læsingu: Ef logging notar innra biðröð, skráarlæsingu eða singleton getur það orðið hluti af deadlock. UI-Callbacks ættu að halda loggingi í lágmarki eða skrifa í sérskilda, ekki-blokkandi loggrás.
Ef þið hafið þegar Layer-3-arkitektúr (UI, Services/Domäne, innviðir eins og gagnaaðgang) þá eiga UI-Callbacks helst aðeins að sjá um UI. Allt sem telst „Service“ á ekki heima í callback-inu. Þetta dregur verulega úr endurkomuáhrifum.
Lokun án hangana: „ekkI WaitFor, heldur samvinnubundin stöðvun“
Við lokun fer þetta oft úrskeiðis: UI lokast, þráður á að hætta, en raðaðir UI-Jobs eru enn ókláraðir. Hreinn lokun fer ekki fram með því að „drepa“ þræði, heldur sem smá samhæfing:
- Stilla shutdown-flagg (t.d.
TUiDispatcher.BeginShutdown): Frá þessu augnabliki engir nýjir UI-Jobs. - Hætta Worker samvinnulega: Workerinn athugar cancel-flagg (t.d.
TEventeðaTCancellationToken-svipað) og bærir niður lykkjur/biðferla. - Ekki blokka UI: Engin hörð bið-lykkja í aðalþræðinum. Ef þarf að „bíða“, þá aðeins með áframhaldandi message loop (eða betra: forðast það með því að meðhöndla lok með callback).
- Síðustu UI-hreinsunarverk aðeins ef gluggar/stýringar eru örugglega enn til. Í VCL skiptir tímasetning höfuðmáli: ekki síðar en þegar handle-inn er horfinn mega raðaðir jobb ekki fara á stýringar.
Þessi röð skiptir máli fyrir rekstur og stuðning: „Forritið hangir við lokun“ er klassískt samþykktarvandamál þótt allt faglega hafi verið unnið rétt. Skýr lokun sparar raunverulegan tíma.
Debugging: Hvernig þú gerir deadlock gripanlegan (án getgátna)
Þegar það hangir er kjarna spurningin: Hver bíður eftir hverjum? Nokkrar aðferðir sem hafa reynst í viðhaldsverkefnum:
- Kortleggja öll Wait-staðsetningar: Volltextsuche nach
WaitFor,Sleepin Schleifen,TEvent.WaitFor,INFINITE. Viele Probleme sind „versteckte“ Waits (auch in Bibliotheken). - Thread-Zustand im Log: Loggen Sie an Thread-Grenzen: „Job hefst“, „UI sett í biðröð“, „UI keyrð“, „Job lokið“. Damit sehen Sie, ob der aðalþráðurinn überhaupt raðað verkefnum vinnur.
- Message-Loop-Verdacht prüfen: Tritt der Hänger nur bei modalen Dialogen oder bestimmten COM-Interaktionen auf, ist die Message Loop oft der Flaschenhals. Dann ist das Ziel: UI-Handler entlasten, COM-Aufrufe isolieren, keine langen Operationen im UI.
- Locks sichtbar machen: Bei
TCriticalSection/TMonitorlohnt sich ein Debug-Build mit „Owner“-Metadaten (z. B. Thread-ID beim Enter) und zeitlicher Messung. So sehen Sie, welches Lock der aðalþráður gerade hält, während Worker auf UI wartet.
Wichtig ist die Haltung: Deadlocks sind selten „zufällig“. Sie sind deterministische Zyklen, die nur selten ausgelöst werden. Wenn Sie den Zyklus einmal sauber identifiziert haben, ist die Behebung meist klar.
Útfærslur für gagnaaðgang und Schnittstellen-Jobs (FireDAC, REST, Dateisystem)
Gerade bei FireDAC (oder anderen DB-Zugriffen) gilt: Verbindung, Transaktion und Datasets sind in der Praxis threadgebunden. Ein Worker-Thread sollte seinen DB-Kontext ausschließlich selbst besitzen. UI-Aufrufe sollten sich auf Darstellung beschränken, nicht auf DB-Operationen. Ein robustes Muster ist:
- Worker führt Query/REST-Call aus, berechnet Ergebnis, erzeugt DTO.
- Worker postet DTO via
Queue/TUiDispatcher.Postan die UI. - UI übernimmt DTO und aktualisiert Controls (ohne Rückgriff auf Worker-Objekte).
Wenn Sie historisch gewachsene Mischformen haben („UI kallar á DB, DB-Callback kallar á UI“), lohnt sich eine schrittweise Entkopplung: Erst Übergabepunkte isolieren (Dispatcher), dann Zustände in Services/Model verlagern. Das ist weniger riskant als ein großer Umbau, aber reduziert Deadlocks spürbar.
Fazit: Deadlocks vermeiden heißt Übergaben kontrollieren
TThread und Synchronize ohne UI-Deadlocks ist weniger eine einzelne Technik als Disziplin: Blockaden minimieren, Lock-Reihenfolgen sauber halten, Shutdown definieren und synchrone UI-Abhängigkeiten reduzieren. Der gezeigte UI-Dispatcher ist in Legacy-Situationen besonders nützlich, weil er Queue als Default nutzt, für notwendige synchrone Übergaben aber Timeout und klare Shutdown-Regeln nachrüstet.
Einsatzgrenzen bleiben: Wenn der aðalþráður dauerhaft blockiert (durch schwergewichtige UI-Logik, modale Dialogketten oder COM-STA-Aufrufe), kann auch ein Dispatcher nur diagnostizieren und kontrolliert abbrechen. Die nachhaltige Lösung ist dann, die UI zu entlasten und Verantwortlichkeiten zu trennen. Wenn Sie dafür in einer bestehenden Delphi-Anwendung Unterstützung brauchen – von Threading-Fallen bis zur schrittweisen Stabilisierung – können Sie das Vorhaben hier einordnen: Projekt oder Modernisierungsvorhaben mit Net-Base besprechen.
Im fachlichen Umfeld spielen auch Delphi Multithreading und Synchronize Deadlock eine wichtige Rolle, wenn Integrationen, Datenflüsse und Weiterentwicklung sauber zusammenspielen müssen.
Samtal um verkefni oder nútímavæðingarverkefni mit Net-Base.