Net-Base Revista

15.05.2026

TThread i Synchronize sense bloquejos de la UI: patrons robustos per a VCL i codi heretat

Com treballar de manera fiable amb TThread, Synchronize i Queue sense que la UI es bloquegi: causes típiques de deadlock, un patró de dispatcher d'UI pràctic (inclòs timeout), protecció en el shutdown, estratègies de bloqueig i comprovacions de depuració per a aplicacions Delphi madures.

15.05.2026

Qui treballa amb threads a Delphi arriba més aviat o més tard a TThread.Synchronize. I precisament allà passen les coses desagradables: bloquejos esporàdics, «UI no respon», deadlocks aparentment aleatoris en tancar o en obrir un quadre de diàleg. El nucli rarament és «Delphi està trencat», sinó gairebé sempre una barreja desafortunada de Synchronize, operacions d’espera bloquejants i un fil de la UI que ja no processa correctament la seva Message Loop (el processament d’esdeveniments de la VCL). Aquest article mostra patrons robustos, pràctics en un context legacy, per a TThread i Synchronize sense deadlocks a la UI — incloent una variant amb timeout, propagació neta d’errors, regles de shutdown i pistes de depuració que ajuden en aplicacions existents reals.

Per què es produeixen deadlocks al voltant de Synchronize a la pràctica

Synchronize vol dir: un worker-thread posa una rutina a la cua que s’executa al fil principal i normalment espera que aquesta rutina acabi. En aplicacions VCL el fil principal és també el fil de la UI (finestres, controls, esdeveniments). A més, en moltes instal·lacions hi corren objectes COM en el STA-Modell (Single-Threaded Apartment: les crides COM s’han de processar al mateix fil), cosa que intensifica la dependència d’una Message Loop que funcioni correctament.

Els deadlocks normalment sorgeixen per una d’aquestes constel·lacions:

  • WaitFor al fil principal: el fil de la UI espera un worker (p. ex. MyThread.WaitFor) mentre el worker necessita el fil de la UI via Synchronize. Tots dos esperen — final.
  • Lock-Inversion: el worker manté un lock (p. ex. TCriticalSection o TMonitor) i crida Synchronize. La rutina sincronitzada a la UI intenta agafar el mateix lock (directa o indirectament, sovint per logging/cache/singletons) — deadlock clàssic.
  • Shutdown/Destroy: en tancar un formulari, un thread es finalitza mentre encara hi ha tasques Synchronize pendents. Especialment problemàtic: les crides sincronitzades fan referència a controls que s’estan destruint.
  • Message Loop bloquejada: diàlegs modals, operacions UI de llarga durada, una crida COM bloquejant o un handler que «rapidamente» fa DB/REST mantenen el fil principal bloquejat. Les tasques Synchronize s’executen amb retard o no s’executen gens.

La conseqüència més important per a l’arquitectura i l’operació: Synchronize és una vora de bloqueig. En software empresarial a mida amb imports, substitució de BDE amb connexió nativa-queries, treballs d’integració o serveis en segon pla amb un component UI, cal controlar conscientment aquesta vora — si no, del «rara vegada» passarà a «sempre quan es té pressa».

Regla fonamental: no fer esperar mai el fil de la UI a un worker (quan s’utilitza Synchronize)

Si un worker fa servir en algun lloc Synchronize, el fil principal no hauria de bloquejar de manera forta esperant aquest worker. Això sembla trivial, però en codi legacy és una de les causes més freqüents, perquè «esperem una mica en tancar» o «el diàleg de progrés espera que acabi» s’hi colen fàcilment.

Conseqüències pràctiques:

  • No cridar WaitFor en el fil d’interfície d’usuari un cop en el Worker existeixi un camí que utilitzi Synchronize.
  • Senyalitzar la finalització del fil mitjançant Event/Callback: la UI es manté reactiva i només neteja després del senyal.
  • Publicar les actualitzacions de la UI fonamentalment mitjançant TThread.Queue o un Dispatcher, perquè els Worker no quedin bloquejats.

TThread.Queue sovint és l’opció per defecte més adequada: el Worker envia feina al fil principal, continua executant-se i no es bloqueja. Això evita molts deadlocks. No obstant això, no resol tots els casos límit — per exemple quan, en un Worker, necessiteu obligatòriament un resultat que es genera en el fil principal (p. ex., accés a un recurs lligat a la UI o a un component que està vinculat a un fil).

TThread i Synchronize sense UI-deadlocks: model mental per a transicions netes

Un model mental robust és: només hi ha poques transferències sincrones legítimes cap al fil principal. Tot la resta és estat, representació o telemetria — i, per tant, asíncron.

Una classificació senzilla ajuda en les revisions i en l’estabilització de projectes existents:

  • «Només mostrar»: progrés, línia de registre, comptador, semàfor, activar/desactivar — sempre Queue.
  • «Transferir estat»: el Worker lliura un objecte de dades/DTO i la UI renderitza — Queue, però amb còpia/immutabilitat (és a dir, sense estructures mutades compartides).
  • «La UI ha de decidir»: només aquí necessiteu semàntica sincrona (p. ex., una consulta a l’usuari). La pregunta real és: ha d’esperar realment un Worker, o es pot redissenyar el flux de treball (State Machine, cancel·lar el Job, reprendre-ho més tard)?

Precisament la tercera categoria és una trampa de deadlock: si el Worker espera un resultat de la UI, la UI sovint es veu temptada a esperar el Worker (o indirectament a través de bloquejos). Això falla amb més facilitat sota càrrega, amb bases de dades lentes o en entorns Remote-Desktop.

Fragment de codi: UI-Dispatcher amb Queue, timeout opcional i apagada neta

El patró següent encapsula les transicions cap a la UI en una petita classe d’ajuda. Obtindreu:

  • Post: fire-and-forget via TThread.Queue (típic per a actualitzacions d’estat).
  • Call: trucada síncrona amb Timeout (inusual, però útil en situacions legacy), sense utilitzar directament Synchronize com a punt de bloqueig.
  • Protecció d’apagada: no acceptar més UI-jobs nous, i els jobs en cua comproven una bandera abans de manipular controls.

Classificació tècnica: utilitzem Queue més TEvent (un Kernel-Event) per a la retroalimentació. El Worker no espera Synchronize, sinó un event que es posa en el fil principal després que s’hagi executat l’acció en cua. El Timeout evita que es quedi „per sempre“ penjat si, per algun motiu, el fil de la UI no canvia d’estat per processar les tasques.

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.

Objectiu del codi i en què és deliberadament «inhabitual»

El patró no substitueix completament Synchronize, però fa que les transferències síncrones siguin controlables: el fil de treball no espera la mecànica de Synchronize, sinó un Event. Això permet forçar timeouts, detectar en execució que el fil de la UI està penjat, i rebutjar de manera consistent els nous treballs de la UI durant la fase de shutdown.

La part «inhabitual» no és l’Event, sinó la decisió de representar la semàntica síncrona amb Queue + Event. Val la pena exactament quan cal reforçar la robustesa de forma progressiva en aplicacions existents, sense poder redissenyar arquitectònicament cada lloc on s’utilitza Synchronize de cop.

Condicions marc i punts crítics

  • Visibilitat de memòria: DoneEvent és el límit de sincronització. Això fa que la lectura de RaisedObj després de WaitFor sigui consistent. Tot i això, RaisedObj ha de romandre local per a cada crida (com aquí), mai global.
  • Gestió d’excepcions: AcquireExceptionObject evita que l’excepció al Main Thread „desaparegui“. En tornar a llençar-la al Worker, l’stacktrace no és idèntic a l’origen, però el missatge d’error es manté al registre del Worker i el treball pot fallar de manera neta.
  • El timeout és diagnosi i protecció: No „repara“ un Main Thread bloquejat. Però evita que els Worker lliguin recursos indefinidament (p. ex. mantenint transaccions BDE-Ablosung mit nativer Anbindung obertes), i fa mesurable la classe d’errors.
  • El shutdown ha de començar aviat: BeginShutdown pertany a una seqüència de shutdown centralitzada (p. ex. molt aviat a OnCloseQuery del formulari principal). Si no, encara es poden posar UI-jobs a la cua mentre les finestres ja estan destruïdes.

Estrategia de locks: com evitar inversions de lock amb callbacks d’UI

Molts deadlocks no sorgeixen per WaitFor, sinó per un ordre de locks poc clar. Flux típic: el Worker bloqueja el «model de dades», fa un update de la UI via Synchronize, l’update de la UI torna a accedir al «model de dades». Té sentit des del punt de vista lògic, però és tècnicament fatal.

Regles pràctiques que es poden establir en equips:

  • No mantenir locks a través de fronteres de thread: Abans que un Worker posi qualsevol cosa a la cua/sincronitzi cap a la UI, els locks de domini han d’haver estat alliberats.
  • La UI llegeix snapshots: Els callbacks d’UI no han de mirar „live“ dins d’estructures del Worker, sinó mostrar còpies/snapshots (p. ex. DTO, record, valors simples).
  • El logging pot ser candidat a lock: Si el logging fa servir internament una cua, un file-lock o un singleton, pot convertir-se en part d’un deadlock. Els callbacks d’UI haurien de mantenir el logging al mínim o escriure a través d’una pipeline de logs separada i no bloquejant.

Si ja disposeu d’una arquitectura Layer-3 (UI, serveis/domini, infraestructura com l’accés a dades): els callbacks d’UI idealment només han de fer tasques de UI. Tot el que sigui „service“ no pertany al callback. Això redueix notablement els efectes de reentrància.

Shutdown sense bloquejos: «no WaitFor, sinó aturada cooperativa»

En tancar sovint es desequilibra: la UI tanca, un thread ha d’acabar, però encara hi ha UI-jobs a la cua. Un shutdown net és menys «matar un thread» i més una petita coreografia:

  1. Posar un flag de shutdown (p. ex. TUiDispatcher.BeginShutdown): a partir d’ara no més UI-jobs nous.
  2. Aturar els workers de forma cooperativa: El worker comprova un cancel-flag (p. ex. TEvent o similar a TCancellationToken) i acaba bucles/esperes.
  3. No bloquejar la UI: Cap bucle d’espera dur al Main Thread. Si cal „esperar“, que sigui només amb la loop de missatges activa (o millor: evitar-ho completament tractant la finalització per callback).
  4. Útims treballs de neteja de la UI només quan les finestres/controls existeixin garantitzadament. En VCL el moment és crític: com a molt tard quan l’handle desapareix, els jobs en cua no poden anar més a controls.

Aquest procediment és rellevant per a l’operació i el suport: „l’aplicació es penja en tancar“ és un problema clàssic d’acceptació, encara que funcionalment tot estigui processat correctament. Un shutdown definit estalvia temps real aquí.

Depuració: Com fer el deadlock tangible (sense endevinalles)

Quan hi ha un bloqueig, la pregunta clau és: qui espera a qui? Algunes aproximacions que han demostrat ser útils en projectes existents:

  • Inventariar tots els punts de wait: cerca de text complet per WaitFor, Sleep en bucles, TEvent.WaitFor, INFINITE. Molts problemes són esperes „ocultes“ (també en biblioteques).
  • Estat del thread al log: registreu als límits del thread: „Job startet“, „queued UI“, „UI ausgeführt“, „Job fertig“. Així podreu veure si el fil principal processa efectivament les tasques en cua.
  • Comprovar sospita de Message Loop: si el bloqueig només es produeix amb diàlegs modals o en certes interaccions COM, el bucle de missatges sovint és l’embut. L’objectiu és: alleugerir els handlers de la UI, aïllar les crides COM i evitar operacions llargues dins de la UI.
  • Fer visibles els locks: amb TCriticalSection/TMonitor val la pena un build de debug amb metadades d'“Owner“ (p. ex. ID del thread en l’Enter) i mesura temporal. Així podreu veure quin lock manté en aquell moment el fil principal mentre els workers esperen la UI.

És important l’actitud: els Deadlocks rarament són „accidentals“. Són cicles deterministes que només s’activen en casos concrets. Un cop identifiqueu de manera neta el cicle, la solució acostuma a ser clara.

Variants per a l’accés a dades i jobs d’interfície (FireDAC, REST, sistema de fitxers)

Precisament amb FireDAC (o altres accessos a BD) s’aplica: connexió, transacció i datasets són a la pràctica lligats al thread. Un Worker-Thread hauria de posseir exclusivament el seu context de BD. Les crides des de la UI s’han de limitar a la presentació, no a operacions sobre la BD. Un patró robust és:

  1. El worker executa la Query/REST-call, calcula el resultat i genera un DTO.
  2. El worker envia el DTO via Queue/TUiDispatcher.Post a la UI.
  3. La UI rep el DTO i actualitza els controls (sense recórrer a objectes del worker).

Si teniu formes mixtes desenvolupades històricament („UI triggert DB, DB-Callback triggert UI“), val la pena un desacoblament progressiu: primer aïllar els punts de transferència (Dispatcher), després traslladar estats a serveis/model. Això és menys arriscat que una reestructuració massiva, però redueix els Deadlocks de manera notable.

Conclusió: evitar Deadlocks vol dir controlar les transferències

TThread und Synchronize ohne UI-Deadlocks no és tant una única tècnica com una disciplina: minimitzar bloqueigs, mantenir neta la seqüència d’adquisició de locks, definir l’shutdown i reduir dependències sincròniques de la UI. El UI-Dispatcher mostrat és especialment útil en situacions legacy perquè usa Queue per defecte, però incorpora Timeout i regles clares d’shutdown per a les transferències sincròniques necessàries.

Hi ha límits d’aplicació: si el fil principal està bloquejat de manera persistent (per lògica UI molt pesada, cadenes de diàlegs modals o crides COM-STA), un Dispatcher només podrà diagnosticar i interrompre de manera controlada. La solució sostenible és alleugerir la UI i separar responsabilitats. Si necessiteu suport en una aplicació existent Delphi —des de trampes de threading fins a l’estabilització pas a pas— podeu situar el projecte aquí: parlar del projecte o de la modernització amb Net-Base.

En l’entorn professional, també tenen un paper important el multithreading de Delphi i el Synchronize Deadlock quan integracions, fluxos de dades i evolució han de funcionar juntes de manera neta.

Parlar del projecte o del pla de modernització amb Net-Base.

Comparteix la publicació

Comparteix aquesta publicació directament

LinkedIn, X, XING, Facebook, WhatsApp i E-Mail estan disponibles de forma immediata. Per a Instagram preparem directament l’enllaç i un text breu.

Correu electrònic

Instagram s'obre en una pestanya nova. L'enllaç i el text curt es copien prèviament al porta-retalls.