Net-Base Revistă

15.05.2026

TThread și Synchronize fără blocaje UI: modele robuste pentru VCL și cod moștenit

Cum să lucrați fiabil cu TThread, Synchronize și Queue fără ca UI să se blocheze: cauze tipice de deadlock, un model pragmatic de UI-dispatcher (inclusiv timeout), protecție la închidere, strategii de blocare și verificări de debugging pentru aplicații Delphi mature.

15.05.2026

Cine lucrează în Delphi cu thread‑uri ajunge mai devreme sau mai târziu la TThread.Synchronize. Și exact acolo apar lucrurile neplăcute: blocări sporadice, „UI nu răspunde“, deadlock‑uri aparent aleatorii la închidere sau la deschiderea unui dialog. Nucleul rar este „Delphi este stricat“, ci aproape întotdeauna un amestec nefavorabil de Synchronize, operațiuni de așteptare blocate și un UI‑Thread care nu mai procesează curat Message Loop‑ul (procesarea evenimentelor VCL). Acest articol arată pattern‑uri robuste, practice în context legacy pentru TThread și Synchronize fără UI‑deadlock‑uri – inclusiv o variantă cu timeout, propagare curată a erorilor, reguli de shutdown și indicații de debugging care ajută în aplicații existente.

De ce apar deadlocks în jurul lui Synchronize în practică

Synchronize înseamnă: un worker‑thread pune o procedură într‑un coadă, care este executată în Main Thread, și așteaptă tipic până când acea procedură se termină. În aplicațiile VCL, Main Thread‑ul este în același timp UI‑Thread‑ul (ferestre, controale, evenimente). În plus, în multe instalări rulează acolo obiecte COM în STA‑Modell (Single‑Threaded Apartment: apelurile COM trebuie procesate în același thread), ceea ce intensifică dependența de o Message Loop funcțională.

Deadlock‑urile apar tipic din una din următoarele configurații:

  • WaitFor în Main Thread: UI‑Thread‑ul așteaptă un worker (de ex. MyThread.WaitFor), în timp ce worker‑ul tocmai are nevoie de UI‑Thread via Synchronize. Amândoi așteaptă – sfârșit.
  • Lock‑Inversion: worker‑ul deține un lock (de ex. TCriticalSection sau TMonitor) și apelează Synchronize. Procedura UI sincronizată încearcă să obțină același lock (direct sau indirect, adesea prin logging/cache/singletons) – deadlock clasic.
  • Shutdown/Destroy: la închiderea unui formular un thread este terminat, în timp ce încă există sarcini Synchronize. În mod deosebit neplăcut: apelurile sincronizate referențiază controale care tocmai sunt distruse.
  • Message Loop blocată: dialogue modale, operațiuni UI de durată, un apel COM blocant sau un handler care „rapid“ face DB/REST țin Main Thread‑ul ocupat. Sarcinile Synchronize sunt procesate cu întârziere sau deloc.

Consecința principală pentru arhitectură și operare: Synchronize este un punct de blocaj. În software‑ul enterprise personalizat cu importuri, BDE-Ablosung mit nativer Anbindung-Queries, joburi de integrare sau servicii de background cu componentă UI, acest punct trebuie controlat conștient – altfel din „raro“ se transformă în „mereu atunci când e urgent“.

Regulă de bază: niciodată să nu lași UI‑Thread‑ul să aștepte un worker (când Synchronize este implicat)

Dacă un worker folosește undeva Synchronize, Main Thread‑ul nu ar trebui să aștepte blocant acel worker. Pare trivial, dar în cod legacy aceasta este una dintre cele mai frecvente cauze, pentru că „așteptăm puțin la închidere“ sau „dialogul de progres așteaptă finalizarea“ sunt introduse rapid.

Consecințe practice:

  • Fără apeluri WaitFor în firul UI, odată ce în Worker există un flux care folosește Synchronize.
  • Semnalizați încheierea firului prin Event/Callback: UI rămâne responsivă, curăță abia după semnal.
  • Actualizările UI, în principiu, postați prin TThread.Queue sau printr-un dispatcher, astfel încât Worker-ul să nu se blocheze.

TThread.Queue este frecvent opțiunea implicită mai bună: Worker-ul postează muncă către firul principal, continuă să ruleze și nu se blochează. Aceasta previne multe deadlock-uri. Nu rezolvă însă toate cazurile limită – de exemplu când într-un Worker aveți nevoie neapărat de un rezultat care este generat în firul principal (de ex. acces la o resursă legată de UI sau o componentă care este thread-bound).

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

Un model mental robust este: există foarte puține predări sincron legitime către firul principal. Tot restul este stare, reprezentare sau telemetrie — și deci asincron.

O clasificare simplă ajută la code reviews și la stabilizarea proiectelor existente:

  • „Doar afișare“: Progres, linie de jurnal, contor, semafor, activare/dezactivare – întotdeauna Queue.
  • „Transmitere stare“: Worker livrează obiect de date/DTO, UI redă – Queue, dar cu copiere/imutaibilitate (deci fără structuri mutate în comun).
  • „UI trebuie să decidă“: Doar aici aveți nevoie de semantică sincronă (de ex. o interogare către utilizator). Întrebarea reală este: Trebuie cu adevărat ca un Worker să aștepte, sau se poate reproiecta fluxul de lucru (mașină de stări, anulare job, reluare mai târziu)?

Fix a treia categorie este o capcană de deadlock: dacă Worker-ul așteaptă un rezultat din UI, UI-ul este tentat rapid să aștepte Worker-ul (sau indirect prin blocări). Aceasta cedează mult mai ușor sub sarcină, la baze de date lente sau în medii Remote Desktop.

Fragment de cod sursă: UI-Dispatcher cu Queue, timeout opțional și shutdown curat

Pattern-ul de mai jos încapsulează predările către UI într-o mică clasă helper. Veți obține:

  • Post: Fire-and-forget prin TThread.Queue (tipic pentru actualizări de stare).
  • Call: apel sincron cu Timeout (neobișnuit, dar util în situații legacy), fără a folosi direct Synchronize ca punct de blocare.
  • Shutdown-Schutz: nu se mai acceptă job-uri UI noi, iar job-urile aflate în coadă verifică un flag înainte de a atinge controalele.

Încadrare tehnică: folosim Queue plus TEvent (un kernel event) pentru feedback. Worker-ul nu așteaptă Synchronize, ci așteaptă un event care este setat în firul principal după ce acțiunea din coadă a fost executată. Timeout-ul previne blocarea „veșnică“ în cazul în care firul UI, dintr-un motiv sau altul, nu mai procesează.

Delphi
unit UiDispatch;

interface

uses
  System.SysUtils,
  System.Classes,
  System.SyncObjs;

type
  EUiDispatchTimeout = class(Exception);
  EUiDispatchShuttingDown = class(Exception);

  /// <summary>
  ///  Învelește apelurile UI din thread-urile de lucru.
  ///  Post: asincron (Queue).
  ///  Call: sincron cu timeout, fără a bloca direct TThread.Synchronize.
  /// </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;

  // În faza de shutdown nu se acceptă noi job-uri UI.
  if IsShuttingDown then
    Exit;

  // Queue nu blochează worker-ul.
  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 este în proces de închidere.');

  DoneEvent := TEvent.Create(nil, True, False, '');
  try
    RaisedObj := nil;

    TThread.Queue(nil,
      procedure
      begin
        try
          if not IsShuttingDown then
            AProc();
        except
          // Transferă obiectul Exception peste granița de thread.
          // Atenție: Nu folosi "raise" aici, altfel ajunge în 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 după %d ms: Main Thread nu a procesat apelul UI.',
          [ATimeoutMs]);
    else
      raise Exception.Create('Status WaitFor neașteptat în UI-Dispatcher.');
    end;
  finally
    DoneEvent.Free;
  end;
end;

end.

Scopul codului și unde este intenționat „neobișnuit”

Pattern-ul nu înlocuiește complet Synchronize, dar face operațiile sincrone controlabile: worker-ul nu așteaptă mecanismul Synchronize, ci un Event. Astfel puteți impune timeouts, puteți evidenția în funcționare că UI-thread-ul este blocat și, în faza de shutdown, puteți refuza consecvent noi job-uri UI.

Partea „neobișnuită” nu este Event-ul, ci decizia de a reprezenta semantica sincronă cu Queue + Event. Aceasta merită exact atunci când trebuie să aduceți stabilitate treptat în aplicații existente, fără a refactoriza arhitectural imediat fiecare apel Synchronize.

Condiții limită și capcane

  • Vizibilitate în memorie: DoneEvent este marginea de sincronizare. Astfel citirea RaisedObj după WaitFor este consistentă. Totuși, RaisedObj ar trebui să rămână local pe apel (așa cum este aici), niciodată global.
  • Exception-Handling: AcquireExceptionObject împiedică ca excepția să „dispară” în Main Thread. La relansarea în Worker, Stacktrace-ul nu este identic cu cel originar, dar mesajul de eroare rămâne în Worker-Log, iar jobul poate eșua în mod controlat.
  • Timeout ist Diagnose und Schutz: El nu „repară” un Main Thread blocat. Prevăz însă că Worker-ii nu țin resurse nelimitat (de ex. BDE-Ablosung mit nativer Anbindung-transacțiuni deschise), și face clasa de eroare măsurabilă.
  • Shutdown muss früh beginnen: BeginShutdown aparține unei secvențe centrale de shutdown (de ex. foarte devreme în OnCloseQuery al formularului principal). Altfel, UI-Jobs vor fi încă în coadă, în timp ce ferestrele sunt deja distruse.

Lock-Strategie: so vermeiden Sie Lock-Inversionen mit UI-Callbacks

Multe deadlock-uri nu apar din cauza WaitFor, ci dintr-o ordine neclară a lock-urilor. Fluxul tipic: Worker blochează „Datenmodell”, apelează un UI-Update prin Synchronize, iar UI-Update accesează din nou „Datenmodell”. E logic din punct de vedere funcțional, dar tehnic fatal.

Reguli practice care se pot impune în echipe:

  • Keine Locks über Thread-Grenzen halten: Înainte ca un Worker să queueze sau să sincronizeze ceva către UI, lock-urile funcționale trebuie eliberate.
  • UI liest Snapshots: Callback-urile UI nu ar trebui să inspecteze „live” structurile Worker-ului, ci să afișeze copii/snapshots (de ex. DTO, Record, valori simple).
  • Logging ist ein Lock-Kandidat: Dacă logging-ul folosește intern o coadă, o blocare de fișier sau un Singleton, poate deveni parte a unui deadlock. Callback-urile UI ar trebui să minimizeze logging-ul sau să scrie printr-o pipeline de log separată, non-blocantă.

Dacă aveți deja o arhitectură Layer-3 (UI, Services/Domäne, infrastructură precum accesul la date): callback-urile UI, ideal, ar trebui să facă doar UI. Tot ce este „Service” nu aparține în callback. Aceasta reduce semnificativ efectele de reentrancy.

Shutdown ohne Hänger: „nicht WaitFor, sondern kooperatives Stoppen”

La închidere lucrurile se strică adesea: UI se închide, un thread trebuie să dispară, dar UI-Jobs aflate în coadă sunt încă deschise. Un shutdown curat nu este „uciderea” unui thread, ci o mică coregrafie:

  1. Shutdown-Flag setzen (z. B. TUiDispatcher.BeginShutdown): De acum înainte nu se mai acceptă UI-Jobs noi.
  2. Worker kooperativ stoppen: Worker-ul verifică un Cancel-Flag (de ex. TEvent sau TCancellationToken-asemănător) și încheie buclele/asteptările.
  3. UI nicht blockieren: Nicio buclă dură de așteptare în firul principal. Dacă trebuie „să așteptați”, faceți-o doar cu o Message Loop care rulează în continuare (sau, și mai bine: evitați complet, tratați finalizarea prin callback).
  4. Letzte UI-Aufräumarbeiten doar dacă ferestrele/controalele există garantat. În VCL momentul este important: cel târziu când Handle-ul dispare, job-urile aflate în coadă nu mai pot accesa controalele.

Acest flux este relevant pentru operare și suport: „Die Anwendung hängt beim Schließen” este o problemă clasică de acceptare, chiar dacă din punct de vedere funcțional totul a fost procesat corect. Un Shutdown definit economisește timp real.

Debugging: Wie Sie den Deadlock greifbar machen (ohne Rätselraten)

Când se blochează, întrebarea centrală este: Cine așteaptă pe cine? Câteva abordări care s-au dovedit în proiecte existente:

  • Inventarierea tuturor punctelor de așteptare: căutare full‑text pentru WaitFor, Sleep în bucle, TEvent.WaitFor, INFINITE. Multe probleme provin din wait‑uri „ascunse” (inclusiv în biblioteci).
  • Starea thread‑ului în log: logați la granițele thread‑urilor: „Job pornește”, „UI pus în coadă”, „UI executat”, „Job finalizat”. Astfel vedeți dacă firul principal procesează efectiv job‑urile aflate în coadă.
  • Verificați suspiciunea asupra Message Loop: dacă blocajul apare doar la dialoguri modale sau la anumite interacțiuni COM, bucla de mesaje este adesea gâtul de sticlă. Obiectivul: degrevați handler‑ele UI, izolați apelurile COM, nu rulați operații lungi în UI.
  • Faceți lock‑urile vizibile: pentru TCriticalSection/TMonitor merită un build de debug cu metadate „Owner” (de ex. ID‑ul thread‑ului la Enter) și măsurare temporală. Astfel vedeți ce lock deține firul principal în timp ce worker‑ii așteaptă UI‑ul.

Este importantă atitudinea: deadlock‑urile rar sunt „accidentale”. Ele sunt cicluri deterministe care se declanșează rar. Odată ce ați identificat curat ciclul, remedierea este de obicei clară.

Variante pentru accesul la date și job‑urile de interfață (FireDAC, REST, sistemul de fișiere)

Mai ales pentru FireDAC (sau alte accesări DB) se aplică: conexiunea, tranzacția și Datasets‑urile sunt în practică legate de thread. Un Worker‑Thread ar trebui să dețină în exclusivitate propriul context DB. Apelurile UI ar trebui să se limiteze la afișare, nu la operații pe DB. Un pattern robust este:

  1. Worker execută interogarea/apelul REST, calculează rezultatul și generează DTO.
  2. Worker postează DTO prin Queue/TUiDispatcher.Post către UI.
  3. UI preia DTO și actualizează controalele (fără apel la obiectele worker).

Dacă aveți forme mixte rezultate istoric („UI declanșează DB, callback‑ul DB declanșează UI”), merită o decuplare treptată: mai întâi izolați punctele de transfer (Dispatcher), apoi mutați stările în Services/Model. Aceasta este mai puțin riscant decât o restructurare majoră, dar reduce deadlock‑urile vizibil.

Concluzie: evitarea deadlock‑urilor înseamnă controlul transferurilor

TThread und Synchronize ohne UI‑Deadlocks nu este atât o tehnică singulară, cât o disciplină: minimizați blocajele, păstrați ordinea de achiziție a lock‑urilor clară, definiți proceduri de shutdown și reduceți dependențele sincrone de UI. UI‑Dispatcher‑ul prezentat este util în situații legacy pentru că folosește Queue ca implicit, iar pentru predări sincrone necesare adaugă Timeout și reguli clare de shutdown.

Există limite de aplicare: dacă firul principal rămâne blocat pe termen lung (din cauza logicii UI greoaie, lanțurilor de dialoguri modale sau apelurilor COM‑STA), chiar și un dispatcher poate doar diagnostica și opri controlat. Soluția durabilă este degrevarea UI și separarea responsabilităților. Dacă aveți nevoie de sprijin pentru asta într‑o aplicație existentă Delphi — de la capcane de threading până la stabilizare treptată — puteți încadra proiectul aici: discutați proiectul sau modernizarea cu Net-Base.

În mediul profesional, multithreading‑ul Delphi și deadlock‑urile Synchronize au, de asemenea, un rol important atunci când integrările, fluxurile de date și evoluția trebuie să funcționeze coerent.

Discutați proiectul sau modernizarea cu Net-Base.

Partajează postarea

Distribuiți această postare direct

LinkedIn, X, XING, Facebook, WhatsApp și e-mail sunt disponibile imediat. Pentru Instagram pregătim linkul și textul scurt imediat.

E-mail

Instagram se deschide într-o filă nouă. Linkul și textul scurt se copiază în prealabil în clipboard.