Kto v Delphi pracuje s vláknami, skôr alebo neskôr skončí pri TThread.Synchronize. A práve tam sa dejú nepríjemnosti: sporadické zaseknutia, „UI nereaguje“, zdánlivo náhodné deadlocky pri ukončovaní alebo pri otváraní dialógu. Jadro zriedka spočíva v tom, že „Delphi je rozbitý“, skôr ide takmer vždy o nevhodnú kombináciu Synchronize, blokujúcich čakacích operácií a UI-Threadu, ktorý už svoju Message Loop (spracovanie udalostí VCL) nedokončuje správne. Tento príspevok ukazuje robustné, v legacy-kontexte praktické vzory pre TThread a Synchronize bez UI-deadlockov – vrátane varianty s timeoutom, korektného prenesenia chýb, pravidiel pre shutdown a tipov na debugovanie, ktoré pomáhajú v reálnych existujúcich aplikáciách.
Prečo v praxi vznikajú deadlocky v súvislosti so Synchronize
Synchronize znamená: pracovné vlákno vloží procedúru do fronty, ktorá sa vykoná v Main Thread, a typicky čaká, kým táto procedúra neskončí. V VCL-aplikáciách je Main Thread zároveň UI-Thread (okná, ovládacie prvky, udalosti). Navyše v mnohých inštaláciách tam bežia COM-objekty v STA-Modell (Single-Threaded Apartment: COM-volania musia byť spracované v tom istom vlákne), čo ešte viac zvyšuje závislosť na fungujúcej Message Loop.
Deadlocky vznikajú typicky v dôsledku jednej z týchto konštelácií:
- WaitFor v Main Thread: UI-Thread čaká na worker (napr.
MyThread.WaitFor), zatiaľ čo worker práve cezSynchronizepotrebuje UI-Thread. Obe čakajú – koniec. - Lock-Inversion: Worker drží zámok (napr.
TCriticalSectionaleboTMonitor) a voláSynchronize. Synchronizovaná UI-procedúra sa pokúsi získať ten istý zámok (priamo alebo nepriamo, často cez logging/cache/singletony) – klasický deadlock. - Shutdown/Destroy: Pri zatváraní formulára sa vlákno ukončuje, zatiaľ čo ešte existujú
Synchronize-úlohy. Obzvlášť nepríjemné: synchronizované volania referencujú ovládacie prvky, ktoré sa práve rušia. - Message Loop blokovaná: Modálne dialógy, dlhé UI-operácie, blokujúce COM-volanie alebo handler, ktorý „len rýchlo“ robí DB/REST, držia Main Thread.
Synchronize-úlohy sa vykonajú oneskorene alebo vôbec nie.
Najdôležitejší dôsledok pre architektúru a prevádzku: Synchronize je hraničná hrana blokovania. V individuálnom podnikovej softvéri s importami, BDE-náhrada s natívnym prepojením-dotazy, úlohami rozhraní alebo službami na pozadí s UI-komponentou by sa táto hrana mala vedome kontrolovať – inak sa z „zriedka“ raz stane „vždy, keď je to naliehavé“.
Základné pravidlo: UI-Thread nesmie čakať na Worker (ak je v hre Synchronize)
Ak worker niekde používa Synchronize, Main Thread by na tento worker nemal tvrdo a blokujúco čakať. Znie to triviálne, ale v legacy kode je to jedna z najčastejších príčin, pretože rýchle pridanie „počkajte pri zatváraní“ alebo „progress dialog čaká na dokončenie“ sa ľahko prehliadne.
Praktické dôsledky:
- Žiadne
WaitFor-volania v UI-vlákne, pokiaľ v workeri existuje cesta, ktorá používaSynchronize. - Ukončenie vlákna signalizovať cez event/callback: UI zostáva responzívne, upratovanie sa vykoná až po signále.
- UI-aktualizácie zásadne postovať cez
TThread.Queuealebo dispatcher, aby worker neblokoval.
TThread.Queue je často lepšia predvolená voľba: Worker pošle prácu do hlavného vlákna, pokračuje v behu a neblokuje. To zabraňuje mnohým deadlockom. Nevyrieši to však všetky okrajové prípady – napríklad keď v workeri nutne potrebujete výsledok, ktorý vytvára hlavné vlákno (napr. prístup k UI-viazanému zdroju alebo ku komponente viazanej na vlákno).
TThread und Synchronize ohne UI-Deadlocks: Denkmodell für saubere Übergaben
Robustný model myslenia je: Existuje len málo legitímnych synchronných odovzdaní do hlavného vlákna. Všetko ostatné je stav, zobrazenie alebo telemetria – a teda asynchrónne.
Jednoduché rozdelenie pomáha pri review a pri stabilizácii existujúcich projektov:
- „Iba zobraziť“: Progress, logový riadok, čítač, stavová dióda, Enable/Disable – vždy
Queue. - „Odovzdať stav“: Worker dodá dátový objekt/DTO, UI vykreslí –
Queue, ale s kopírovaním/immutability (t. j. žiadne spoločne mutované štruktúry). - „UI musí rozhodnúť“: Len tu potrebujete synchronnú sémantiku (napr. dotaz používateľovi). Potom je zásadná otázka: Musí naozaj worker čakať, alebo sa dá workflow preusporiadať (stavový automat, zrušiť job, pokračovať neskôr)?
Práve tretia kategória je pasca na deadlock: Ak worker čaká na výsledok z UI, UI je rýchlo v pokušení čakať na worker (alebo nepriamo cez locky). To pri zaťažení, pomalých databázach alebo v Remote-Desktop prostrediach vedie k zlyhaniu výraznejšie.
Ukážka zdrojového kódu: UI-Dispatcher s Queue, voliteľným Timeoutom a čistým Shutdownom
Následujúci vzor zapuzdruje odovzdania do UI v malej pomocnej triede. Získate:
- Post: Fire-and-forget cez
TThread.Queue(typické pre stavové aktualizácie). - Call: Synchronous Call s Timeout (neobvyklé, ale užitočné v legacy situáciách), bez priameho použitia
Synchronizeako blokačného bodu. - Shutdown-ochrana: Už neprijímať nové UI-joby a queued joby kontrolujú flag pred zásahom do ovládacích prvkov.
Technické zaradenie: Používame Queue plus TEvent (kernel-event) na spätnú väzbu. Worker nečaká na Synchronize, ale na event, ktorý nastaví hlavné vlákno po vykonaní queued akcie. Timeout zabráni „nekonečnému“ zaveseniu, ak UI-vlákno z nejakého dôvodu prestane spracovávať.
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.Účel kódu a kde je zámerne „neobvyklý“
Tento vzor nenahrádza Synchronize úplne, ale robí synchronné odovzdania kontrolovateľnými: pracovné vlákno nečaká na mechanizmus Synchronize, ale na Event. Takto môžete vynútiť časové limity, za prevádzky zistiť, že vlákno UI je zablokované, a v štádiu ukončovania dôsledne odmietať nové UI úlohy.
Neobvyklá časť nie je Event, ale rozhodnutie mapovať synchronnú sémantiku pomocou Queue + Event. Toto sa oplatí práve vtedy, keď musíte v existujúcich aplikáciách postupne zvyšovať stabilitu bez nutnosti okamžite prestavovať každé miesto so Synchronize na architektonickej úrovni.
Okrajové podmienky a úskalia
- Viditeľnosť v pamäti:
DoneEventje synchronizačná hranica. Vďaka tomu je čítanieRaisedObjpoWaitForkonzistentné. Napriek tomu byRaisedObjmalo zostať lokálne pre každé volanie (ako tu), nikdy globálne. - Spracovanie výnimiek:
AcquireExceptionObjectzabraňuje tomu, aby výnimka v Main Thread „zmizla“. Pri opätovnom vyvolaní vo Workerovi nie je stack trace totožný s pôvodom, ale chybové hlásenie zostáva v worker-logu a úloha môže skončiť čistým zlyhaním. - Timeout je diagnostika a ochrana: „Neopraví“ zablokovaný Main Thread. Zabraňuje však tomu, aby Worker viazali zdroje donekonečna (napr. BDE-Ablosung mit nativer Anbindung-transakcie ostávali otvorené), a robí triedu chyby merateľnou.
- Shutdown musí začať skoro:
BeginShutdownpatrí do centrálnej shutdown-sekvencie (napr. veľmi skoro vOnCloseQueryhlavného formulára). Inak sa ešte UI-úlohy zaradia do fronty, zatiaľ čo okná už sú zničené.
Stratégia zámkov: ako sa vyhnúť inverziám zámkov pri UI-callbackoch
Mnoho deadlockov nevzniká kvôli WaitFor, ale kvôli nejasnému poradiu zámkov. Typický priebeh: Worker uzamkne „dátový model“, vyvolá aktualizáciu UI cez Synchronize, ktorá opäť pristupuje k „dátovému modelu“. To je logicky pochopiteľné, no technicky fatálne.
Praktické pravidlá, ktoré sa dajú presadiť v tímoch:
- Nedržať zámky cez hranice vlákien: Skôr než Worker niečo pošle do UI do fronty alebo synchronizuje, mali by byť uvoľnené aplikačné zámky.
- UI číta snapshoty: UI-callbacky by nemali „live“ nazerať do worker-štruktúr, ale zobrazovať kópie/snapshoty (napr. DTO, Record, jednoduché hodnoty).
- Logging je kandidát na zámok: Ak logging interne používa frontu, súborový zámok alebo singleton, môže sa stať súčasťou deadlocku. UI-callbacky by mali logging minimalizovať alebo zapisovať cez samostatnú, neblokujúcu log-pipeline.
Ak už máte Layer-3-architektúru (UI, Services/Domäne, infraštruktúra ako prístup k dátam): UI-callbacky by ideálne mali robiť len UI. Všetko, čo je „Service“, nepatrí do callbacku. To výrazne znižuje efekty reentrancie.
Shutdown bez zaseknutí: „nie WaitFor, ale kooperatívne zastavenie“
Pri ukončovaní to často zlyhá: UI sa zatvára, vlákno by malo skončiť, ale zaradené UI-úlohy sú ešte otvorené. Čistý shutdown nie je „zabiť vlákno“, ale malá choreografia:
- Nastaviť shutdown-flag (napr.
TUiDispatcher.BeginShutdown): Od teraz žiadne nové UI-úlohy. - Worker kooperatívne zastaviť: Worker kontroluje cancel-flag (napr.
TEventaleboTCancellationToken-podobný) a ukončí slučky/čakania. - Neblokovať UI: Žiadne tvrdé čakacie slučky v Main Thread. Ak musíte „čakať“, tak len so stále bežiacou slučkou spracovania správ (alebo lepšie: úplne sa tomu vyhnúť a spracovať dokončenie cez callback).
- Posledné UI-úpravy len ak okná/kontrolky garantovane ešte existujú. Vo VCL je načasovanie dôležité: najneskôr keď handle zmizne, zaradené úlohy už nesmú pristupovať ku kontrolkám.
Tento postup je relevantný pre prevádzku a podporu: „Aplikácia sa zaseká pri zatváraní“ je klasický akceptačný problém, hoci funkčne bolo všetko spracované správne. Definovaný shutdown tu reálne šetrí čas.
Debugging: Ako spraviť deadlock uchopiteľným (bez hádania)
Keď sa to zasekne, jadrová otázka znie: Kto čaká na koho? Niekoľko prístupov, ktoré sa osvedčili v existujúcich projektoch:
- Inventarizovať všetky miesta čakania: Volltextsuche nach
WaitFor,Sleepin Schleifen,TEvent.WaitFor,INFINITE. Viele Probleme sind „versteckte“ Waits (auch in Bibliotheken). - Stav vlákna v logu: Zaznamenávajte v logu na hraniciach vlákien: „Job startet“, „queued UI“, „UI ausgeführt“, „Job fertig“. Tak uvidíte, či hlavné vlákno vôbec spracováva zaradené úlohy.
- Skontrolovať podozrenie na message loop: Ak sa zaseknutie vyskytuje len pri modálnych dialógoch alebo pri určitých COM-interakciách, často je úzkym miestom smyčka správ. Cieľom je: odľahčiť UI-handlery, izolovať COM-volania, nevykonávať dlhé operácie v UI.
- Zviditeľniť zámky: Pri
TCriticalSection/TMonitorsa oplatí Debug-Build s „Owner“-metadátami (napr. ID vlákna pri Enter) a meraním časov. Tak uvidíte, ktorý zámok hlavné vlákno drží, keď pracovnícke vlákna čakajú na UI.
Dôležitý je prístup: deadlocky sú zriedka „náhodné“. Ide o deterministické cykly, ktoré sa len zriedka spúšťajú. Ak cyklus raz správne identifikujete, oprava je väčšinou jasná.
Varianty prístupu k dátam a rozhraniam pre joby (FireDAC, REST, Dateisystem)
Najmä pri FireDAC (alebo iných prístupoch k DB) platí: pripojenie, transakcia a Datasets sú v praxi viazané na vlákno. Pracovné vlákno by malo svoj DB-kontext vlastniť výlučne samo. Volania z UI by sa mali obmedziť na zobrazenie, nie na DB-operácie. Robustný vzor je:
- Worker vykoná Query/REST-call, spočíta výsledok, vytvorí DTO.
- Worker postuje DTO cez
Queue/TUiDispatcher.Postna UI. - UI prevezme DTO a aktualizuje ovládacie prvky (bez spätného pristupu k objektom workeru).
Ak máte historicky vzniknuté zmiešané formy („UI spúšťa DB, DB-callback spúšťa UI“), oplatí sa krokovo oddeliť zodpovednosti: najprv izolovať body odovzdania (dispatcher), potom presunúť stavy do služieb/modelu. To je menej rizikové než veľká refaktorizácia, no výrazne znižuje výskyt deadlockov.
Záver: Predchádzať deadlockom znamená kontrolovať odovzdávanie
TThread und Synchronize ohne UI-Deadlocks je menej jedna technika a viac disciplína: minimalizovať blokovania, udržať poradie zámkov čisté, definovať shutdown a zredukovať synchronné závislosti na UI. Ukázaný UI-Dispatcher je v legacy-situáciách obzvlášť užitočný, pretože štandardne používa Queue, a pre nevyhnutné synchronné odovzdania doplní Timeout a jasné pravidlá pre shutdown.
Obmedzenia zostávajú: ak je hlavné vlákno trvalo zablokované (ťažkou UI-logikou, reťazcami modálnych dialógov alebo COM-STA-volaniami), dispatcher dokáže len diagnostikovať a kontrolovane prerušiť. Trvalé riešenie je odľahčiť UI a rozdeliť zodpovednosti. Ak na to potrebujete podporu v existujúcej Delphi‑aplikácii – od nástrah pri threading až po postupnú stabilizáciu – môžete projekt alebo modernizačný zámer tu zaradiť: Projekt oder Modernisierungsvorhaben mit Net-Base besprechen.
V odbornom kontexte zohrávajú aj Delphi Multithreading a Synchronize‑deadlock dôležitú úlohu, keď integrácie, dátové toky a ďalší vývoj musia spolupracovať čisto.
Projekt oder Modernisierungsvorhaben mit Net-Base besprechen.