Net-Base Magazín

15.05.2026

TThread a Synchronize bez UI-deadlocků: robustní vzory pro VCL a legacy kód

Jak spolehlivě pracovat s TThread, Synchronize a Queue, aniž by UI přestalo reagovat: typické příčiny deadlocků, prakticky použitelný UI-dispatcher vzor (včetně timeoutu), ochrana při ukončování, strategie zámků a ladicí kontroly pro existující Delphi-aplikace.

15.05.2026

Kdo v Delphi pracuje s vlákny, dříve či později narazí na TThread.Synchronize. A právě tam se dějí nepříjemné věci: sporadické zasekávání, „UI nereaguje“, zdánlivě náhodné deadlocky při ukončování nebo při otevírání dialogu. Jádro problému zřídka bývá „Delphi je kaputt“, téměř vždy jde o nevhodnou kombinaci Synchronize, blokujících čekacích operací a UI-Threadu, který svou Message Loop (zpracování událostí VCL) už neprovádí správně. Tento článek ukazuje robustní, v kontextu legacy praktikovatelná schémata pro TThread a Synchronize bez UI-deadlocků – včetně varianty s timeoutem, čistého předávání chyb, pravidel pro shutdown a tipů pro debugování, které pomáhají v reálných existujících aplikacích.

Proč v praxi vznikají deadlocky kolem Synchronize

Synchronize znamená: pracovní vlákno zařadí proceduru do fronty, která se vykoná v Main Thread, a typicky čeká, dokud tato procedura neskončí. V VCL aplikacích je Main Thread zároveň UI-Thread (okna, ovládací prvky, události). Navíc v mnoha instalacích běží v tomto vlákně COM objekty v režimu STA (Single-Threaded Apartment: volání COM musí být zpracována ve stejném vlákně), což ještě zvyšuje závislost na fungující Message Loop.

Deadlocky obvykle vznikají v jedné z těchto konstelací:

  • WaitFor im Main Thread: UI-Thread čeká na worker (např. MyThread.WaitFor), zatímco worker právě přes Synchronize potřebuje UI-Thread. Oba čekají – konec.
  • Lock-Inversion: Worker drží zámek (např. TCriticalSection nebo TMonitor) a volá Synchronize. Synchronizovaná UI procedura se snaží získat tentýž zámek (přímo nebo nepřímo, často přes logging/cache/singletony) – klasický deadlock.
  • Shutdown/Destroy: Při zavírání formuláře je vlákno ukončeno, zatímco stále čekají úlohy Synchronize. Zvlášť nebezpečné: synchronizovaná volání odkazují na Controls, které jsou právě ničeny.
  • Message Loop blockiert: Modální dialogy, dlouhotrvající UI operace, blokující COM volání nebo handler, který si „jen tak“ dělá DB/REST, zablokují Main Thread. Synchronize-úlohy jsou zpracovány opožděně nebo vůbec.

Nejdůležitější důsledek pro architekturu a provoz: Synchronize je hranice blokování. V individuálním podnikových řešeních s importy, BDE-Ablosung mit nativer Anbindung-dotazy, integračními joby nebo background službami s UI komponentou by měla být tato hrana vědomě kontrolována – jinak se z „zřídka“ nakonec stane „vždy, když je to naléhavé“.

Grundregel: UI-Thread nie auf Worker warten lassen (wenn Synchronize im Spiel ist)

Pokud pracovní vlákno někde používá Synchronize, neměl by Main Thread tvrdě blokujícím způsobem na tento worker čekat. Zní to triviálně, ale v legacy kódu je to jedna z nejčastějších příčin, protože „počkejme chvilku při zavírání“ nebo „dialog průběhu čeká na konec“ se rychle přidá.

Praktické konsekvence:

  • Žádné WaitFor-volání ve vláknu UI, jakmile v pracovním vlákně existuje cesta, která používá Synchronize.
  • Ukončení vlákna signalizovat přes Event/Callback: UI zůstane responsivní, úklid proběhne až po přijetí signálu.
  • UI-aktualizace zásadně posílat přes TThread.Queue nebo přes Dispatcher, aby pracovní vlákna nebyla blokována.

TThread.Queue je často lepší výchozí volba: Worker pošle práci do hlavního vlákna, pokračuje ve svém běhu a není blokován. To zabrání mnoha deadlockům. Neřeší to však všechny okrajové případy – například když ve workeru nutně potřebujete výsledek, který je vytvořen v hlavním vlákně (např. přístup k UI-vázanému zdroji nebo komponentě, která je vázána na vlákno).

TThread a Synchronize bez UI-deadlocků: myšlenkový model pro čisté předávání

Robustní myšlenkový model je: Existuje jen pár oprávněně synchronních předání do hlavního vlákna. Vše ostatní je stav, zobrazení nebo telemetrie – a tedy asynchronní.

Jednoduché rozdělení pomáhá při revizích a při stabilizaci stávajících projektů:

  • „Pouze zobrazení“: průběh, řádek logu, čítač, semafor, povolit/zakázat – vždy Queue.
  • „Předat stav“: Worker dodá datový objekt/DTO, UI vykreslí – Queue, ale s kopií/immutabilitou (tedy žádné společně měněné struktury).
  • „UI musí rozhodnout“: Jen zde potřebujete synchronní sémantiku (např. dotaz uživateli). Pak je skutečná otázka: Musí pracovní vlákno opravdu čekat, nebo lze workflow přestavět (stavový automat, úlohu zrušit, později pokračovat)?

Právě třetí kategorie je pastí na deadlocky: Když worker čeká na výsledek z UI, UI je snadno sváděna k tomu čekat na workera (nebo nepřímo přes zámky). To se při zátěži, pomalých databázích nebo v prostředích Remote-Desktop projeví mnohem snáze.

Ukázka zdrojového kódu: UI-Dispatcher s Queue, volitelným Timeoutem a čistým ukončením

Následující vzor zapouzdřuje předávání do UI v malé pomocné třídě. Získáte:

  • Post: fire-and-forget přes TThread.Queue (typické pro aktualizace stavu).
  • Call: synchronní volání s Timeoutem (neobvyklé, ale užitečné v legacy situacích), aniž by se přímo používalo Synchronize jako bod blokace.
  • Shutdown-Schutz: nepřijímat nové UI-úlohy a zařazené úlohy kontrolují příznak před manipulací s ovládacími prvky.

Technické zařazení: Používáme Queue plus TEvent (kernelový event) pro zpětnou vazbu. Worker nečeká na Synchronize, ale na event, který je nastaven v hlavním vlákně poté, co byla vykonána zařazená akce. Timeout zabraňuje „věčnému“ zaseknutí, pokud z nějakého důvodu vlákno UI přestane úlohy zpracovávat.

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 proč je úmyslně „neobvyklý“

Vzor nenahrazuje úplně Synchronize, ale umožňuje kontrolovat synchronní předání: pracovní vlákno nečeká na mechanismus Synchronize, nýbrž na událost. Tím můžete vynucovat časové limity, za provozu zjistit, že UI vlákno zamrzlo, a v průběhu fáze ukončování odmítat nové UI úlohy.

„Neobvyklou“ částí není samotná událost, ale rozhodnutí modelovat synchronní sémantiku pomocí Queue + Event. To se vyplatí právě tehdy, když u stávajících aplikací postupně zvyšujete stabilitu, aniž byste museli okamžitě architektonicky přebudovat každé místo se Synchronize.

Hraniční podmínky a úskalí

  • Paměťová viditelnost: DoneEvent je synchronizační hranou. Díky tomu je čtení RaisedObj po WaitFor konzistentní. Přesto by RaisedObj měl zůstat lokální pro volání (jak je zde), nikdy globální.
  • Zpracování výjimek: AcquireExceptionObject brání tomu, aby výjimka v Main Thread „zmizela“. Při opětovném vyhození ve Workeru není stacktrace totožný s původem, ale chybová zpráva zůstane v logu Workeru a úloha může korektně selhat.
  • Timeout je diagnostika a ochrana: Ne „opraví“ zablokované Main Thread. Brání ale tomu, aby Worker neomezeně vázal zdroje (např. BDE-Ablosung mit nativer Anbindung-transakce otevřené), a činí tuto třídu chyb měřitelnou.
  • Shutdown musí začít brzy: BeginShutdown patří do centrální shutdown-sekvence (např. velmi brzy v OnCloseQuery hlavní formy). Jinak se budou ještě UI-jobs zařazovat do fronty, zatímco okna jsou už zničená.

Strategie zámků: jak zabránit inverzím zámků u UI-callbacků

Mnoho deadlocků nevzniká kvůli WaitFor, ale kvůli nejasnému pořadí zámků. Typický průběh: Worker zamkne „datový model“, vyvolá UI-aktualizaci přes Synchronize, UI-aktualizace znovu přistupuje k „datovému modelu“. To je logicky srozumitelné, ale technicky fatální.

Praktická pravidla, která lze prosadit v týmech:

  • Žádné zámky přes hranice vláken: Než Worker cokoli směrem k UI zařadí/synchronizuje, měly by být uvolněny věcné zámky.
  • UI čte snapshoty: UI-callbacky by neměly „naživo“ nahlížet do struktur Workeru, ale zobrazovat kopie/snapshoty (např. DTO, Record, jednoduché hodnoty).
  • Logování je kandidát na zámek: Pokud logování interně používá frontu, file-lock nebo singleton, může se stát součástí deadlocku. UI-callbacky by měly logování držet na minimu nebo zapisovat přes samostatnou, neblokující log-pipeline.

Pokud už máte Layer-3-architekturu (UI, Services/Domäne, infrastruktura jako přístup k datům): UI-callbacky by ideálně měly dělat pouze UI. Vše, co je „Service“, nepatří do callbacku. To výrazně snižuje reentrancy-efekty.

Shutdown bez zaseknutí: „ne WaitFor, ale kooperativní zastavení“

Při ukončování to často zadrhne: UI se zavírá, vlákno má zmizet, ale do fronty jsou ještě otevřené UI-úlohy. Čisté ukončení není tolik „zabití vlákna“, jako malá choreografie:

  1. Nastavit shutdown-flag (např. TUiDispatcher.BeginShutdown): Od teď žádné nové UI-úlohy.
  2. Worker kooperativně zastavit: Worker kontroluje cancel-flag (např. TEvent nebo TCancellationToken-podobný) a ukončí smyčky/čekání.
  3. Neblokovat UI: Žádná tvrdá čekací smyčka v Main Thread. Pokud musíte „čekat“, pak pouze s pokračující message loop (nebo lépe: zcela se tomu vyhnout tím, že dokončení ošetříte přes callback).
  4. Poslední UI-úklidové práce pouze pokud okna/ovládací prvky jistě ještě existují. Ve VCL je načasování důležité: nejpozději když je Handle pryč, nesmí být queued úlohy směrovány na ovládací prvky.

Tento postup je relevantní pro provoz a support: „Aplikace se zasekne při zavírání“ je klasický akceptační problém, i když bylo vše věcně správně zpracováno. Definovaný shutdown tu skutečně šetří čas.

Ladění: Jak učinit deadlock hmatatelným (bez hádání)

Když to zadrhne, klíčová otázka je: Kdo na koho čeká? Několik přístupů, které se osvědčily v existujících projektech:

  • Inventarizovat všechna místa s čekáním: Volltextsuche nach WaitFor, Sleep in Schleifen, TEvent.WaitFor, INFINITE. Viele Probleme sind „versteckte“ Waits (auch in Bibliotheken).
  • Stav vlákna v logu: Zaznamenávejte na hranicích vláken: „Job startet“, „queued UI“, „UI ausgeführt“, „Job fertig“. Tím zjistíte, zda hlavní vlákno vůbec zpracovává zařazené úlohy.
  • Prověřit podezření na Message Loop: Objeví-li se zaseknutí pouze u modálních dialogů nebo při určitých COM-interakcích, bývá často úzké místo ve smyčce zpráv. Cílem je pak: odlehčit zpracování UI, izolovat volání COM, neprovádět dlouhé operace v UI.
  • Zámky zviditelnit: Bei TCriticalSection/TMonitor se vyplatí debug build s metadaty „Owner“ (např. ID vlákna při Enter) a časovým měřením. Tak uvidíte, který zámek hlavní vlákno právě drží, zatímco pracovní vlákna čekají na UI.

Důležité je přístupové nastavení: Deadlocky nejsou často „náhodné“. Jsou to deterministické cykly, které se jen zřídka projeví. Pokud cyklus jednou správně identifikujete, je oprava zpravidla jasná.

Varianty pro přístup k datům a úlohy rozhraní (FireDAC, REST, souborový systém)

Zvláště u FireDAC (nebo jiných přístupů k DB) platí: připojení, transakce a datasety jsou v praxi vázané na vlákno. Pracovní vlákno by mělo vlastnit svůj DB-kontext výhradně samo. Volání z UI by se měla omezit na zobrazení, nikoli na DB-operace. Robustní vzor je:

  1. Pracovní vlákno provede Query/REST-volání, vypočítá výsledek, vytvoří DTO.
  2. Pracovní vlákno pošle DTO přes Queue/TUiDispatcher.Post do UI.
  3. UI převezme DTO a aktualizuje ovládací prvky (bez návratu na objekty pracovního vlákna).

Pokud máte historicky vzniklé smíšené vzory („UI spouští DB, DB-callback spouští UI“), vyplatí se postupné oddělování: nejprve izolovat body předávání (Dispatcher), potom přesouvat stavy do služeb/modelu. To je méně rizikové než rozsáhlá přestavba, ale výrazně snižuje výskyt deadlocků.

Závěr: Předcházení deadlockům znamená kontrolu předávání

TThread a Synchronize bez UI-deadlocků jsou méně jedna technika než disciplína: minimalizovat blokace, udržovat pořadí zámků, definovat shutdown a redukovat synchronní závislosti na UI. Ukázaný UI-Dispatcher je v legacy situacích zvlášť užitečný, protože jako výchozí používá Queue, pro nutná synchronní předání však doplňuje Timeout a jasná pravidla ukončení.

Limity zůstávají: Pokud je hlavní vlákno trvale zablokované (vlivem těžkopádné UI-logiky, řetězců modálních dialogů nebo COM-STA volání), může dispatcher pouze diagnostikovat a kontrolovaně ukončit. Trvalé řešení je odlehčit UI a oddělit odpovědnosti. Pokud v existující Delphi aplikaci potřebujete podporu – od pastí při threading až po postupnou stabilizaci – můžete záměr zařadit zde: Projekt nebo Modernisierungsvorhaben mit Net-Base besprechen.

V odborném prostředí hrají také Delphi multithreading a Synchronize-Deadlock důležitou roli, když integrace, datové toky a další vývoj musí hladce spolupracovat.

Projekt oder Modernisierungsvorhaben mit Net-Base besprechen.

Sdílet příspěvek

Sdílet tento příspěvek přímo

LinkedIn, X, XING, Facebook, WhatsApp a e-mail jsou ihned k dispozici. Pro Instagram připravíme odkaz a stručný text.

E-mail

Instagram se otevře v nové záložce. Odkaz a krátký text budou předtím zkopírovány do schránky.