Net-Base Revistë

15.05.2026

TThread dhe Synchronize pa UI-Deadlocks: modele të qëndrueshme për VCL dhe kod legacy

Si të punoni me TThread, Synchronize dhe Queue në mënyrë të besueshme pa bllokuar UI-në: shkaqet tipike të deadlock, një model praktik UI-Dispatcher (përfshirë Timeout), mbrojtje gjatë Shutdown, strategjitë e bllokimit dhe kontrollet për debugim për aplikacione të zhvilluara Delphi.

15.05.2026

Kush punon në Delphi me Threads, më herët apo më vonë do të përballet me TThread.Synchronize. Dhe pikërisht aty ndodhin problemet e pakëndshme: ngecje sporadike, „UI nuk përgjigjet“, deadlock-e që duken rastësore gjatë mbylljes ose kur hapet një dialog. Thelbi rrallëherë është „Delphi është prishur“, por pothuajse gjithmonë një kombinim i pafavorshëm i Synchronize, operacioneve bllokuese pritëse dhe një UI-Thread që nuk përpunon më si duhet Message Loop (përpunimi i ngjarjeve i VCL). Ky artikull tregon modele të forta, praktikueshme në kontekst legacy për TThread und Synchronize ohne UI-Deadlocks – duke përfshirë një variant me timeout, përçim të qartë të gabimeve, rregulla për shutdown dhe këshilla për debug që ndihmojnë në aplikacione reale të përdorura në prodhim.

Pse krijohen Deadlocks rreth Synchronize në praktikë

Synchronize do të thotë: një worker-thread vendos një procedurë në një radhë që ekzekutohet në Main Thread dhe tipikisht pret derisa ajo procedurë të përfundojë. Në aplikacionet VCL, Main Thread është njëkohësisht UI-Thread (dritaret, kontrollet, ngjarjet). Për më tepër, në shumë instalime atje ekzekutohen objekte COM në STA-Modell (Single-Threaded Apartment: thirrjet COM duhet të përpunohen në të njëjtin thread), gjë që rrit varësinë nga një Message Loop funksionale.

Deadlock-et krijohen tipikisht përmes një prej këtyre skenarëve:

  • WaitFor im Main Thread: UI-Thread pret për një worker (p.sh. MyThread.WaitFor), ndërsa worker-i sapo ka nevojë për UI-Thread përmes Synchronize. Të dy presin – përfundim.
  • Lock-Inversion: Worker-i mban një lock (p.sh. TCriticalSection ose TMonitor) dhe thërret Synchronize. Procedura e sinkronizuar në UI përpiqet të marrë të njëjtin lock (drejtpërdrejt ose indirekt, shpesh përmes logging/cache/singletons) – deadlock klasik.
  • Shutdown/Destroy: Gjatë mbylljes së një forme, një thread përfundohet ndërsa ende ka detyra Synchronize në pritje. Veçanërisht problematik: thirrjet e sinkronizuara referojnë kontrolle që sapo po shkatërrohen.
  • Message Loop blockiert: Dialogë modalë, operacione UI që zgjasin shumë, një thirrje COM që bllokon ose një handler që “thjesht bën” DB/REST, mbajnë Main Thread-in të zënë. Zadatat Synchronize përpunohen me vonesë ose nuk ekzekutohen fare.

Konseguenca më e rëndësishme për arkitekturën dhe operimin: Synchronize është një kufi bllokues. Në softuerin e personalizuar të ndërmarrjeve me importe, BDE-Ablosung me lidhje native-queries, detyra për ndërfaqe ose shërbime background me komponent UI, ky kufi duhet të kontrollohet me vetëdije – përndryshe nga „rrallë“ bëhet „përherë kur ka ngut”.

Rregulla themelore: Mos lejoni që UI-Thread të presë Worker-in (kur Synchronize është në lojë)

Nëse një worker përdor diku Synchronize, Main Thread nuk duhet të bllokohet fuqishëm duke pritur për atë worker. Kjo duket triviale, por në kodin legacy është një nga shkaqet më të zakonshme, sepse fraza të tilla si „presim pak gjatë mbylljes“ ose „dialogu i progresit pret për përfundim“ vendosen shpejt.

Konsekencat praktike:

  • Keine WaitFor-Aufrufe im UI-Thread, sobald im Worker ein Pfad existiert, der Synchronize nutzt.
  • Thread-Abschluss per Event/Callback signalisieren: UI bleibt responsiv, räumt erst nach Signal auf.
  • UI-Updates grundsätzlich über TThread.Queue oder 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 Synchronize als 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.

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.

Qëllimi i kodit dhe pse është qëllimisht “i pazakontë”

Ky model nuk zëvendëson plotësisht Synchronize, por ai bën të kontrollueshme transferimet sinkrone: thread-i i punës nuk pret mekanikën e Synchronize, por pret një Event. Në këtë mënyrë mund të detyroni timeoute, të bëni të dukshme gjatë operimit që UI-thread-i është i bllokuar, dhe gjatë fazës së shutdown të refuzoni në mënyrë konsekuente punët e reja të UI-së.

Pjesa “e pazakontë” nuk është Event-i, por vendimi për të modeluar semantikën sinkrone me Queue + Event. Kjo ia vlen veçanërisht kur në aplikacione ekzistuese duhet të përmirësoni gradualisht stabilitetin, pa riprojektuar menjëherë në mënyrë arkitektonike çdo vend ku përdoret Synchronize.

Kushtet kufizuese dhe kurthet

  • Dukshmëria e memories: DoneEvent është pika e sinkronizimit. Për këtë arsye, leximet e RaisedObj pas WaitFor janë konsistente. Megjithatë, RaisedObj duhet të mbetet lokale për çdo thirrje (si këtu), kurrë globale.
  • Trajtimi i përjashtimeve: AcquireExceptionObject parandalon që përjashtimi të „zhduket“ në Main Thread. Kur hidhet përsëri në Worker, Stacktrace nuk është identik me origjinën, por mesazhi i gabimit mbetet në Worker-Log, dhe Job mund të dështojë në mënyrë të pastër.
  • Timeout është diagnozë dhe mbrojtje: Ai nuk „rregullon“ një Main Thread të bllokuar. Por parandalon që Worker-et të zënë pafundësisht burime (p.sh. të mbajnë transaksione BDE-Ablosung mit nativer Anbindung të hapura), dhe e bën klasën e gabimit të matshme.
  • Mbyllja duhet të fillojë herët: BeginShutdown i takon një sekuence qendrore të mbylljes (p.sh. shumë herët në OnCloseQuery të formularit kryesor). Përndryshe UI-Jobs ende do të vihen në radhë, ndërsa dritaret tashmë po shkatërrohen.

Strategjia e bllokimit: si të shmangni inverzionet e bllokimit me UI-Callback-e

Shumë deadlock-e nuk lindin për shkak të WaitFor, por për shkak të një rendi të paqartë të bllokimeve. Rrjedha tipike: Worker-i bllokon „modelin e të dhënave“, thërret përditësimin e UI-së me Synchronize, përditësimi i UI-së qaset përsëri tek „modeli i të dhënave“. Kjo është logjikisht e kuptueshme, por teknikisht fatale.

Rregulla praktike që mund të zbatohen në ekipe:

  • Mos mbani bllokime përtej kufijve të Thread-it: Para se një Worker të vërë diçka në radhë/sinkronizojë me UI-në, bllokimet funksionale duhet të jenë liruar.
  • UI lexon kopje (Snapshots): Callback-et e UI-së nuk duhet të shikojnë „live“ strukturat e Worker-it, por duhet të shfaqin kopje/snapshots (p.sh. DTO, Record, vlera të thjeshta).
  • Logging është kandidat për bllokim: Nëse logging-u i brendshëm përdor një queue, file-lock ose një singleton, ai mund të bëhet pjesë e një deadlock-u. Callback-et e UI-së duhet të mbajnë logging-un minimal ose të shkruajnë përmes një pipeline-i të veçantë të logimit që nuk bllokon.

Nëse tashmë keni një arkitekturë Layer-3 (UI, Services/Domäne, infrastrukturë si qasja në të dhëna): Callback-et e UI-së idealisht duhet të kryejnë vetëm punë të UI-së. Çdo gjë që është „Service“ nuk i takon callback-it. Kjo redukton ndjeshëm efektet e reentrancy.

Mbyllje pa bllokime: „jo WaitFor, por ndalje kooperative“

  1. Vendosni flag-un e mbylljes (p.sh. TUiDispatcher.BeginShutdown): Nga tani e tutje asnjë UI-Job i ri nuk lejohet.
  2. Ndalohet kooperativisht Worker-i: Worker-i kontrollon një flag anulimi (p.sh. TEvent ose i ngjashëm me TCancellationToken) dhe përfundon ciklet/pritjet.
  3. Mos bllokoni UI-në: Asnjë cikël pritjeje i ashpër në Main Thread. Nëse duhet të „prisni“, atëherë vetëm me një Message Loop që vazhdon (ose edhe më mirë: evitoni plotësisht, duke trajtuar përfundimin përmes callback-it).
  4. Punët e fundit të pastrimit të UI-së vetëm kur dritaret/controls ekzistojnë të garantuara. Në VCL koha është kritike: së paku kur handle-i është larguar, job-et e vëna në radhë nuk duhet të adresohen më tek kontrollet.

Ky proces është relevant për Operimin dhe Supportin: „Aplikacioni ngec gjatë mbylljes“ është një problem klasik i pranimit, edhe pse në aspektin funksional gjithçka është përpunuar saktë. Një mbyllje e përcaktuar kursen këtu kohë reale.

Debugging: Si ta bëni deadlock-un të prekshëm (pa hamendje)

Kur ngec, pyetja thelbësore është: Kush pret kë? Disa qasje që kanë provuar veten në projekte ekzistuese:

  • Alle Wait-Stellen inventarisieren: Volltextsuche nach WaitFor, Sleep in Schleifen, TEvent.WaitFor, INFINITE. Viele Probleme sind ‚versteckte‘ Waits (auch in Bibliotheken).
  • Thread-Zustand im Log: Loggen Sie an Thread-Grenzen: „Job startet“, „queued UI“, „UI ausgeführt“, „Job fertig“. Damit sehen Sie, ob der Main Thread queued Jobs überhaupt abarbeitet.
  • 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/TMonitor lohnt sich ein Debug-Build mit ‚Owner‘-Metadaten (z. B. Thread-ID beim Enter) und zeitlicher Messung. So sehen Sie, welches Lock der Main Thread 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.

Varianten für Datenzugriff 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:

  1. Worker führt Query/REST-Call aus, berechnet Ergebnis, erzeugt DTO.
  2. Worker postet DTO via Queue/TUiDispatcher.Post an die UI.
  3. UI übernimmt DTO und aktualisiert Controls (ohne Rückgriff auf Worker-Objekte).

Wenn Sie historisch gewachsene Mischformen haben („UI triggert DB, DB-Callback triggert 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 Main Thread 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.

Projekt oder Modernisierungsvorhaben mit Net-Base besprechen.

Ndaje postimin

Shpërndaj këtë postim drejtpërdrejt

LinkedIn, X, XING, Facebook, WhatsApp dhe E‑Mail janë menjëherë të disponueshme. Për Instagram po përgatitim menjëherë lidhjen dhe tekstin e shkurtër.

Postë elektronike

Instagram hapet në një skedë të re. Linku dhe teksti i shkurtër kopjohen më parë në memorjen e kopjimit.