Net-Base Magazín

15.05.2026

TThread a Synchronize bez UI-deadlockov: robustné vzory pre VCL a legacy kód

Ako spoľahlivo pracovať s TThread, Synchronize a Queue bez zasekávania UI: typické príčiny deadlockov, praxou overený UI-dispatcher vzor (vrátane timeoutu), ochrana pri vypínaní, stratégie zamykania a diagnostické kontroly pre dlhodobo vyvíjané Delphi aplikácie.

15.05.2026

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 cez Synchronize potrebuje UI-Thread. Obe čakajú – koniec.
  • Lock-Inversion: Worker drží zámok (napr. TCriticalSection alebo TMonitor) 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žíva Synchronize.
  • 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.Queue alebo 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 Synchronize ako 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ť.

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.

Úč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: DoneEvent je synchronizačná hranica. Vďaka tomu je čítanie RaisedObj po WaitFor konzistentné. Napriek tomu by RaisedObj malo zostať lokálne pre každé volanie (ako tu), nikdy globálne.
  • Spracovanie výnimiek: AcquireExceptionObject zabraň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: BeginShutdown patrí do centrálnej shutdown-sekvencie (napr. veľmi skoro v OnCloseQuery hlavné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:

  1. Nastaviť shutdown-flag (napr. TUiDispatcher.BeginShutdown): Od teraz žiadne nové UI-úlohy.
  2. Worker kooperatívne zastaviť: Worker kontroluje cancel-flag (napr. TEvent alebo TCancellationToken-podobný) a ukončí slučky/čakania.
  3. 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).
  4. 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, Sleep in 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/TMonitor sa 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:

  1. Worker vykoná Query/REST-call, spočíta výsledok, vytvorí DTO.
  2. Worker postuje DTO cez Queue/TUiDispatcher.Post na UI.
  3. 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.

Zdieľať príspevok

Tento príspevok priamo zdieľať

LinkedIn, X, XING, Facebook, WhatsApp a e-mail sú ihneď k dispozícii. Pre Instagram pripravujeme priamo odkaz a krátky text.

E-mail

Instagram sa otvorí v novej karte. Odkaz a krátky text sa predtým skopírujú do schránky.