Net-Base Magazin

15.05.2026

TThread és Synchronize UI-deadlockok nélkül: robosztus minták VCL-hez és legacy kódhoz

Hogyan dolgozhat megbízhatóan TThread, Synchronize és Queue használatával anélkül, hogy a felhasználói felület (UI) lefagyna: tipikus deadlock-ok okai, egy gyakorlatias UI-Dispatcher-minta (timeouttal), leállásvédelem, zárolási stratégiák és hibakeresési ellenőrzések organikusan fejlődött Delphi alkalmazásokhoz.

15.05.2026

Ha valaki a Delphi-ben szálakkal dolgozik, előbb-utóbb a TThread.Synchronize-nél köt ki. És pont ott történnek a kellemetlenségek: szórványos akadozások, „a felület nem reagál”, látszólag véletlenszerű deadlockok bezáráskor vagy egy párbeszédablak megnyitásakor. A probléma ritkán az, hogy „Delphi hibás”, sokkal gyakrabban egy kedvezőtlen keveréke a Synchronize-nek, blokkoló váró műveleteknek és egy UI-szálnak, amely már nem dolgozza fel tisztán a Message Loop-ját (a VCL eseményfeldolgozását). Ez a bejegyzés robosztus, legacy-környezetben gyakorlatban alkalmazható mintákat mutat be TThread és Synchronize UI-deadlockok nélkül – beleértve timeout-változatot, tiszta hibaterjesztést, leállítási szabályokat és hibakeresési jelzéseket, amelyek valódi meglévő alkalmazásoknál hasznosak.

Miért keletkeznek a gyakorlatban deadlockok a Synchronize körül

Synchronize azt jelenti: egy worker-szál egy eljárást tesz egy várólistára, amelyet a fő szál hajt végre, és tipikusan vár addig, amíg az eljárás be nem fejeződik. VCL-alkalmazásokban a fő szál egyben a UI-szál (ablakok, vezérlők, események). Ráadásul sok telepítésnél ott futnak COM-objektumok az STA-Modell szerint (Single-Threaded Apartment: a COM-hívásokat ugyanabban a szálban kell feldolgozni), ami tovább erősíti a működő Message Loop-tól való függést.

A deadlockok tipikusan az alábbi konstellációk valamelyikéből adódnak:

  • WaitFor a fő szálon: a UI-szál egy workerre vár (pl. MyThread.WaitFor), miközben a worker éppen Synchronize-on keresztül a UI-szálat igényli. Mindketten várnak – vége.
  • Lock-Inversion: a worker tart egy lockot (pl. TCriticalSection vagy TMonitor) és meghívja a Synchronize-t. A szinkronizált UI-procedúra megpróbálja megszerezni ugyanazt a lockot (közvetlenül vagy közvetve, gyakran naplózás/cache/singletonok miatt) – klasszikus deadlock.
  • Shutdown/Destroy: egy űrlap bezárásakor egy szál leáll, miközben még fennállnak Synchronize-feladatok. Különösen kellemetlen, ha a szinkronizált hívások olyan vezérlőkre hivatkoznak, amelyeket éppen törölnek.
  • Message Loop blokkolva: modális dialógusok, hosszú futású UI-műveletek, egy blokkoló COM-hívás vagy egy handler, amely „csak gyorsan“ DB/REST műveletet végez, fogva tartja a fő szálat. A Synchronize-feladatok késve vagy egyáltalán nem kerülnek feldolgozásra.

A legfontosabb következmény az architektúra és az üzemeltetés számára: Synchronize blokkoló él. Egyedi vállalati szoftvereknél, amelyek importokat, BDE-kiváltást natív csatlakozással-lekérdezéseket, interfész-feladatokat vagy UI-komponenst tartalmazó háttérszolgáltatásokat végeznek, ezt az élt tudatosan kell kontrollálni – különben a „ritkán“ előbb-utóbb „mindig akkor, amikor sürgős“ lesz.

Alapszabály: a UI-szál soha ne várjon egy workerre (ha Synchronize szerepel)

Ha egy worker valahol Synchronize-t használ, a fő szálnak nem szabad keményen, blokkoló módon várnia erre a workerre. Ez triviálisnak tűnik, de a legacy-kódban ez az egyik leggyakoribb ok: a „várjunk egy kicsit bezáráskor“ vagy a „folyamatjelző dialógus a befejezésre vár“ megoldásokat gyorsan beépítik.

Gyakorlati következmények:

  • Keine WaitFor-Aufrufe im UI-Thread, sobald im Worker ein Pfad existiert, der Synchronize nutzt.
  • Jelezze a szál befejeződését Event/Callback segítségével: a UI reagálókész marad, és csak a jelzés után takarít fel.
  • UI-frissítéseket alapvetően über TThread.Queue vagy egy Dispatcher-en keresztül postoljon, damit Worker nicht blockieren.

TThread.Queue gyakran jobb alapértelmezett opció: a Worker munkát postol a főszálra, folytatja a futást és nem blokkol. Ez sok Deadlock-ot megelőz. Nem old meg minden szélső esetet – például ha egy Workerben feltétlenül szüksége van egy eredményre, amit a főszál állít elő (pl. hozzáférés egy UI-hoz kötött erőforráshoz vagy egy threadhez kötött komponenshez).

TThread und Synchronize ohne UI-Deadlocks: Denkmodell für saubere Übergaben

Egy megbízható gondolkodásmód: csak néhány jogosult szinkron átadás létezik a főszálnak. Minden más állapot, megjelenítés vagy telemetria – és így aszinkron.

Egy egyszerű felosztás segít a review-kor és a meglévő projektek stabilizálásakor:

  • „Csak megjelenítés”: folyamatjelző, naplósor, számláló, jelzőlámpa, engedélyezés/letiltás – mindig Queue.
  • „Állapot átadása”: a Worker adatobjektumot/DTO-t ad, a UI renderel – Queue, de másolással/immutabilitással (tehát nincs közösen módosított struktúra).
  • „A UI-nak kell döntenie”: csak itt van szükség szinkron szemantikára (pl. felhasználói kérdés). Az igazi kérdés ilyenkor: tényleg várnia kell-e egy Workernek, vagy átalakítható a workflow (állapotgép, munka megszakítása, későbbi folytatás)?

Különösen a harmadik kategória Deadlock-csapda: ha a Worker egy UI-eredményre vár, a UI könnyen csábulhat arra, hogy a Workerre várjon (vagy közvetve lockokon keresztül). Ez terhelés alatt, lassú adatbázisoknál vagy Remote-Desktop-környezetben jóval könnyebben kifordul.

Source-Schnipsel: UI-Dispatcher mit Queue, optionalem Timeout und sauberem Shutdown

A következő minta becsomagolja az UI-átadásokat egy kis segédosztályba. Kapni fogja:

  • Post: fire-and-forget über TThread.Queue (tipikus státuszfrissítésekhez).
  • Call: szinkron hívás Időkorláttal (szokatlan, de legacy helyzetekben hasznos), anélkül hogy közvetlenül Synchronize-t használnánk blokkoló pontként.
  • Shutdown-védelem: ne fogadjon több új UI-feladatot, és a sorban álló feladatok ellenőrizzenek egy flaget, mielőtt vezérlőket módosítanak.

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.

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.

A kód célja és miért szándékosan „szokatlan”

A minta nem helyettesíti teljesen a Synchronize-t, de ellenőrizhetővé teszi a szinkron átadást: a worker nem a Synchronize-mechanikára vár, hanem egy Event-re. Így képesek lehetnek időkorlátokat érvényesíteni, üzem közben láthatóvá tenni, ha a UI-szál akadozik, és a leállási fázisban következetesen elutasítani új UI-feladatokat.

A „szokatlan” rész nem maga az Event, hanem az a döntés, hogy a szinkron szemantikát Queue + Event kombinációval modellezzük. Ez akkor éri meg, ha meglévő alkalmazásokban fokozatosan kell stabilitást bevezetni anélkül, hogy minden Synchronize-helyet azonnal architektúráilag újra kellene tervezni.

Korlátozások és buktatók

  • Memória láthatósága: DoneEvent a szinkronizációs határ. Ez biztosítja, hogy a WaitFor után a RaisedObj olvasása konzisztens legyen. Ennek ellenére a RaisedObj-nak hívásonként lokálisnak kell maradnia (ahogy itt is), soha ne legyen globális.
  • Kivételkezelés: AcquireExceptionObject megakadályozza, hogy a kivétel eltűnjön a főszálon. Amikor újradobják a Workerben, a stacktrace nem lesz azonos az eredetivel, de a hibaüzenet megmarad a Worker-logban, és a feladat tisztán elbukhat.
  • Timeout: diagnózis és védelem: Ez nem „javítja” a blokkolt főszálat. Viszont megakadályozza, hogy a workerek korlátlanul erőforrásokat foglaljanak (pl. BDE-Ablosung mit nativer Anbindung-tranzakciókat nyitva tartsanak), és mérhetővé teszi a hibakategóriát.
  • A leállítást korán kell kezdeni: BeginShutdown része legyen egy központi leállítási szekvenciának (pl. nagyon korán a fő űrlap OnCloseQuery eseményében). Ellenkező esetben még UI-munkák kerülnek a sorba, miközben az ablakok már el lettek távolítva.
  • Lock-stratégia: így kerülhetők el a lock-inverziók UI-callbackekkel

    Sok holtpont nem a WaitFor-tól keletkezik, hanem a nem egyértelmű lock-sorrendtől. Tipikus menet: a Worker zárolja az adatmodellt, UI-frissítést hív meg Synchronize-val, a UI-frissítés ismét hozzáfér az adatmodellhez. Logikailag érthető, de technikailag végzetes.

    Gyakorlati szabályok, amelyek a csapatoknál érvényesíthetők:

    • Ne tartsunk lockokat szálhatárokon át: Mielőtt egy Worker bármit az UI felé sorba állítana vagy szinkronizálna, a szakmai lockokat fel kell oldani.
    • Az UI snapshotokat olvasson: Az UI-callbackek ne nézzenek „élőben” a Worker-struktúrákba, hanem másolatokat/snapshotokat jelenítsenek meg (pl. DTO, Record, egyszerű értékek).
    • A naplózás potenciális lock: Ha a naplózás belsőleg sort, fájlzárat vagy singletont használ, része lehet egy holtpontnak. Az UI-callbackek tartózkodjanak a kiterjedt naplózástól, vagy külön, nem blokkoló naplópipeline-ra írjanak.

    Ha már van egy Layer-3-architektúrája (UI, Services/Domäne, infrastruktúra, például adatelérés): az UI-callbackek ideálisan csak UI-t végezzenek. Minden, ami „Service”, ne legyen a callbackben. Ez jelentősen csökkenti a reentrancy-hatásokat.

    Leállítás akadás nélkül: „nicht WaitFor, sondern kooperatives Stoppen”

    A leállításnál gyakran elromlik: az UI bezárul, egy szálnak el kell tűnnie, de még vannak sorban lévő UI-munkák. Egy tiszta leállítás nem a szálak erőszakos megszüntetése, hanem egy kis koreográfia:

    1. Leállítási jelző beállítása (pl. TUiDispatcher.BeginShutdown): Innentől ne induljanak új UI-munkák.
    2. Worker kooperatív leállítása: A Worker ellenőrzi egy lemondási jelzőt (pl. TEvent vagy TCancellationToken-szerű) és befejezi a ciklusokat/várakozásokat.
    3. Ne blokkolja az UI-t: Ne legyen kemény várakozó ciklus a főszálban. Ha „várni kell”, akkor csak futó üzenetkezelő hurok mellett (vagy még jobb: teljesen elkerülni, és a befejezést callbackkel kezelni).
    4. Utolsó UI-takarítási műveletek csak akkor, ha az ablakok/vezérlők garantáltan még léteznek. VCL-ben az időzítés fontos: legkésőbb amikor a handle eltűnik, a sorba állított feladatok már nem léphetnek rá a vezérlőkre.

    Ez a folyamat üzemeltetés és support szempontjából releváns: „Az alkalmazás beragad a bezárásnál” klasszikus elfogadási probléma, még ha szakmailag minden helyesen feldolgozásra került is. Egy definiált leállítás itt valódi időt takarít meg.

    Hibakeresés: hogyan tehető a holtpont megfoghatóvá (rejtvényfejtés nélkül)

    Ha beragad, a kulcskérdés: Ki vár kire? Néhány megközelítés, amelyek beváltak meglévő projektekben:

    • Az összes várakozó helyet feltérképezni: Teljes szövegű keresés a WaitFor, Sleep ciklusokban, TEvent.WaitFor, INFINITE után. Sok probléma „rejtett” várakozásokból ered (akár könyvtárakban is).
    • Thread-állapot a logban: Logoljon a szálhatároknál: „Feladat elindult”, „UI sorba állítva”, „UI végrehajtva”, „Feladat befejeződött”. Így látható, dolgozza-e egyáltalán a Main Thread a queue-olt feladatokat.
    • Message-Loop gyanú ellenőrzése: Ha a lefagyás csak modális párbeszédablakoknál vagy bizonyos COM-interakcióknál jelentkezik, gyakran a Message Loop a szűk keresztmetszet. A cél ilyenkor: tehermentesíteni az UI-kezelőket, izolálni a COM-hívásokat és nem végezni hosszú műveleteket az UI-ban.
    • Lockok láthatóvá tétele: TCriticalSection/TMonitor esetén érdemes egy debug build-et használni „Owner” metaadatokkal (pl. belépéskor a Thread-ID) és időméréssel. Így látható, melyik lockot tartja épp a Main Thread, miközben a worker-ek az UI-ra várnak.

    Fontos hozzáállás: a deadlockok ritkán „véletlenszerűek”. Determinisztikus ciklusok eredményei, amelyek csak ritkán aktiválódnak. Ha egyszer tisztán azonosítja a ciklust, a javítás rendszerint egyértelmű.

    Variánsok adat-hozzáférésre és interfész-feladatokra (FireDAC, REST, fájlrendszer)

    Különösen FireDAC (vagy más DB-hozzáférések) esetén igaz: a kapcsolat, tranzakció és a datasetek gyakorlatban szálhoz kötöttek. Egy worker-szálnak kizárólag a saját DB-környezetét kell birtokolnia. Az UI-hívásokra korlátozódjon a megjelenítés, ne végezzenek DB-műveleteket. Egy robusztus minta:

    1. Worker végrehajt egy Query/REST-hívást, kiszámolja az eredményt, létrehozza a DTO-t.
    2. A worker DTO-t postol a UI felé Queue/TUiDispatcher.Post segítségével.
    3. Az UI átveszi a DTO-t és frissíti a vezérlőket (Controls) — visszahivatkozás nélkül a worker-objektumokra.

    Ha történelmileg kevert megoldások vannak („UI triggereli az DB-t, DB-callback triggereli az UI-t”), érdemes lépésről lépésre szétválasztani: először izolálni az átadási pontokat (Dispatcher), majd az állapotokat áthelyezni service-ekbe/modelbe. Ez kevésbé kockázatos, mint egy nagymértékű átépítés, de jelentősen csökkenti a deadlockokat.

    Következtetés: a deadlockok elkerülése = átadások kontrollja

    TThread és Synchronize nélkül UI-deadlockok elkerülése inkább fegyelem, mint egyetlen technika: minimalizálni a blokkolást, következetesen tartani a lock-sorrendet, definiálni a leállítást és csökkenteni a szinkron UI-függőségeket. A bemutatott UI-Dispatcher legacy helyzetekben különösen hasznos, mert alapértelmezettként a Queue-t használja, de a szükséges szinkron átadásokhoz utólag biztosít Timeout-ot és egyértelmű shutdown-szabályokat.

    Az alkalmazhatóság korlátai megmaradnak: ha a Main Thread tartósan blokkolt (nehézsúlyú UI-logika, modális dialógus-láncok vagy COM-STA-hívások miatt), egy Dispatcher legfeljebb diagnosztizálni tud és kontrolláltan megszakítani. A fenntartható megoldás ilyenkor az UI tehermentesítése és a felelősségek szétválasztása. Ha ebben egy meglévő Delphi-alkalmazásban támogatásra van szüksége – a threading csapdáktól a lépésenkénti stabilizálásig – az ügyet itt sorolhatja be: Projekt vagy Modernisierungsvorhaben mit Net-Base besprechen.

    A szakterületi környezetben szintén fontos szerepet játszanak a Delphi Multithreading és a Synchronize Deadlock kérdései, ha az integrációk, adatszállítások és a továbbfejlesztés tisztán kell, hogy együttműködjenek.

    Projekt vagy Modernisierungsvorhaben mit Net-Base besprechen.

    Bejegyzés megosztása

    Ezt a bejegyzést közvetlenül megosztani

    LinkedIn, X, XING, Facebook, WhatsApp és E-Mail azonnal elérhetők. Instagramra a linket és a rövid szöveget közvetlenül előkészítjük.

    E-mail

    Az Instagram egy új lapon nyílik meg. A link és a rövid szöveg előzetesen a vágólapra másolódik.