Net-Base Ajakiri

15.05.2026

TThread ja Synchronize ilma UI-lukustusseisudeta: robustsed mustrid VCL-i ja pärandkoodi jaoks

Kuidas TThreadi, Synchronize'i ja Queue'ga usaldusväärselt töötada ilma, et UI hanguks: tüüpilised deadlock'i põhjused, praktiline UI-dispatcher-muster (sh timeout), sulgemiskaitse, lukustamisstrateegiad ja debugimise kontrollid väljakujunenud Delphi-rakenduste jaoks.

15.05.2026

Kes Delphiiga threadidega töötab, jõuab varem või hiljem TThread.Synchronize-ini. Just seal tekivad ebameeldivused: sporaadilised hangid, „UI ei reageeri“, näiliselt juhuslikud deadlockid rakenduse sulgemisel või dialoogi avamisel. Põhjus ei ole harva „Delphi on katki“, vaid peaaegu alati ebasoovitav segu Synchronize’ist, blokeerivatest ootetegevustest ja UI-lõimest, mis ei tööta oma Message Loopi (VCL-i sündmuste töötlemine) puhtalt läbi. Käesolev kirjutis näitab robuustseid, legacy-kontekstis rakendatavaid mustreid TThread ja Synchronize ilma UI-deadlockideta — kaasa arvatud timeout-variant, puhas veolevitus, shutdown-reeglid ja debugimisvihjed, mis aitavad tõelistes olemasolevates rakendustes.

Miks Synchronize’i ümber praktikas deadlockid tekivad

Synchronize tähendab: worker-lõim asetab protseduuri järjekorda, mis täidetakse im Main Threadis, ja ootab tavaliselt, kuni see protseduur on lõpetatud. VCL-rakendustes on Main Thread samal ajal UI-lõim (aknad, komponendid, sündmused). Lisaks jooksevad paljudes paigaldustes seal COM-objektid STA-Modellis (Single-Threaded Apartment: COM-kõned peavad toimuma samas lõimes), mis tugevdab sõltuvust toimivast Message Loopist.

Deadlockid tekivad tavaliselt ühe järgmistest konfiguratsioonidest:

  • WaitFor im Main Thread: UI-lõim ootab workerit (nt MyThread.WaitFor), samal ajal kui worker vajab Synchronize kaudu UI-lõime. Mõlemad ootavad — lõpp.
  • Lock-Inversion: worker hoiab lukku (nt TCriticalSection või TMonitor) ja kutsub Synchronize. Sünkroniseeritud UI-protseduur üritab sama lukku võtta (otse või kaudselt, sageli logimise/vahemälu/singletonite kaudu) — klassikaline deadlock.
  • Shutdown/Destroy: vormi sulgemisel lõpetatakse lõim, samal ajal kui Synchronize-ülesanded on veel järjekorras. Eriti ohtlik: sünkroniseeritud kutsed viitavad komponentidele, mida just hävitatakse.
  • Message Loop blockiert: modaalidialoogid, pikaajalised UI-operatsioonid, blokeeriv COM-kõne või handler, mis „kiirelt“ teeb DB/REST, hoiavad Main Threadi kinni. Synchronize-ülesandeid töödeldakse hilinenult või üldse mitte.

Olulisim järeldus arhitektuuri ja operatsiooni jaoks: Synchronize on blokeeriv serv. Individuaalses ettevõtterakenduses, kus on impordid, BDE-Ablosung mit nativer Anbindung-päringud, liidese-tööd või taust-teenused koos UI-komponendiga, tuleks seda servi teadlikult kontrollida — muidu muutub „harva“ kunagi „igal ajal, kui on kiire“.

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

Kui worker kuskil kasutab Synchronizei, ei tohiks Main Thread tingimata seda workerit tahkelt blokeerides oodata. See kõlab ilmse asjana, kuid legacy-koodis on see üks levinumaid põhjusi, sest „oodame veidi sulgemisel“ või „progress-dialoog ootab lõppu“ lükatakse kiiresti sisse.

Praktische Konsequenzen:

  • UI-lõimus ei tohi olla WaitFor-kõnesid, kui Workeris eksisteerib tee, mis kasutab Synchronize.
  • Lõime lõpp signaalida Event/Callback kaudu: UI jääb reageerivaks ja puhastab alles pärast signaali.
  • UI-uuendused postitada põhimõtteliselt läbi TThread.Queue või dispatcheri, et Workerid ei blokeeruks.

TThread.Queue on sageli parem vaikimisi-valik: Worker postitab tööd Main Threadi, jätkab tööd ega blokeeru. See väldib paljusid deadlock’e. Kuid see ei lahenda kõiki äärejuhtumeid – näiteks kui teil on Workeris hädasti vaja tulemust, mida Main Threadis genereeritakse (nt juurdepääs UI-ga seotud ressursile või komponendile, mis on lõimuga seotud).

TThread ja Synchronize ilma UI-deadlock’ideta: mõttemudel korrektsete üleandmiste jaoks

Vastupidav mõttemudel on: Main Threadi tehakse vaid vähe õigustatud sünkroonse üleandmisi. Kõik muu on staatus, esitlus või telemeetria – seega asünkroonne.

Lihtne jaotus aitab ülevaatustes ja olemasolevate projektide stabiliseerimisel:

  • „Ainult kuvamine“: edenemine, logirida, loendur, semafor, aktiveerimine/deaktiveerimine – alati Queue.
  • „Seisundi edastamine“: Worker annab andmeobjekti/DTO, UI renderdab – Queue, kuid koos koopia/muutumatuse põhimõttega (ehk mitte ühismuudetavad struktuurid).
  • „UI peab otsustama“: Ainult siin vajate sünkroonset semantikat (nt kasutajapäring). Siis on tegelik küsimus: kas Worker peab tõesti ootama või saab töövoogu ümber kujundada (olekuautomaat, töö katkestada, hiljem jätkata)?

Eriti kolmas kategooria on deadlocki lõks: kui Worker ootab UI-tulemust, kaldub UI kiiresti Workerit ootama (või kaudselt lukustuste kaudu). See avaldub koormuse all, aeglaste andmebaaside või Remote-Desktop-keskkondade korral märgatavalt tõenäolisemalt.

Koodilõik: UI-dispatcher koos Queue, valikulise timeouti ja korraliku sulgemisega

Järgmine muster kapseldab UI-üleandmised väikesesse abiklassi. Saate:

  • Post: Fire-and-forget läbi TThread.Queue (tüüpiliselt seisundiuuenduste jaoks).
  • Call: sünkroonne kõne koos Timeoutiga (ebaharilik, kuid pärandiolukordades kasulik), ilma et kasutataks otse Synchronize‚i blokeerimispunktina.
  • Shutdown-Schutz: enam uusi UI-töid ei võeta vastu ning järjekorras olevad tööd kontrollivad lippu enne, kui nendega puututakse (nt komponentidega).

Tehniline paigutus: kasutame Queue pluss TEvent (kerneli sündmus) tagasiside jaoks. Worker ei oota Synchronizei, vaid Eventi, mida Main Thread seab pärast seda, kui järjekorda pandud toiming on täidetud. Timeout hoiab ära igavese hangumise, kui UI-lõim mingil põhjusel ei jõua töid töödelda.

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.

Koodi eesmärk ja koht, kus see on teadlikult „ebaharilik“

See muster ei asenda täielikult Synchronize, kuid see teeb sünkroonsete ülekanete kontrollitavaks: Worker ei oota Synchronize-mehhanismi, vaid sündmust. Nii saate nõuda ajapiiranguid, töös nähtavaks teha, et UI-thread on kinni jäänud, ja shutdown-faasis uued UI-tööd järjekindlalt tagasi lükata.

„Ebaharilik“ osa ei ole Event, vaid otsus kirjeldada sünkroonset semantikat läbi Queue + Event. See on põhjendatud just siis, kui peate olemasolevates rakendustes järk-järgult lisama stabiilsust, ilma et peaks iga Synchronize-Stelle kohe arhitektuuriliselt ümber ehitama.

Piirtingimused ja lõksud

  • Mälunähtavus: DoneEvent on sünkroniseerimispiir. Tänu sellele on RaisedObj lugemine pärast WaitFor konsistentne. Sellegipoolest peaks RaisedObj jääma lokaalseks iga Call’i kohta (nagu siin), mitte globaalseks.
  • Erandite käitlemine: AcquireExceptionObject takistab erandi „kadumist“ põhilõimus. Kui erand Workeris uuesti visatakse, ei ole stacktrace identne algallikaga, kuid veateade jääb Worker-logi ja töö võib korrektselt ebaõnnestuda.
  • Timeout on diagnostika ja kaitse: see ei „paranda“ blokeeritud põhilõime. Kuid see takistab Workeritel ressursse piiramatus mahus sidumast (nt BDE-Ablosung mit nativer Anbindung-tehingute avana hoidmine) ning muudab veaklassi mõõdetavaks.
  • Seiskamine peab varakult algama: BeginShutdown kuulub tsentraalsesse seiskamissekvenssi (nt väga varakult OnCloseQuery-s põhivormis). Muul juhul pannakse UI-töid järjekorda samal ajal, kui aknad on juba hävitatud.

Lukustusstrateegia: kuidas vältida lock-inversioone UI-callbackidega

Paljud deadlockid ei teki WaitFor tõttu, vaid ebaselgest lukustuste järjekorrast. Tüüpiline käik: Worker lukustab „andmemudeli“, kutsub UI-uuenduse läbi Synchronize, UI-uuendus pääseb uuesti „andmemudelile“ ligi. See on loogiliselt mõistetav, kuid tehniliselt saatuslik.

Praktilised reeglid, mida meeskonnas rakendada saab:

  • Ärge hoidke lukke lõimipiiride üle: Enne kui Worker midagi UI-sse järjekorda paneb või sünkroniseerib, peaksid rakenduslikud lukud vabanenud olema.
  • UI loeb hetktõmmiseid: UI-callbackid ei tohiks „live“ vaadata Workerite struktuure, vaid kuvada koopiaid/hetktõmmiseid (nt DTO, Record, lihtväärtused).
  • Logimine võib olla lukustusala: Kui logimine kasutab sisemiselt järjekorda, faili-lukku või singletoni, võib see saada deadlocki osaks. UI-callbackid peaksid logimist minimaalsena hoidma või kirjutama eraldi, mitteblokeerivasse logipipeline’i.

Kui teil juba on Layer-3-arhitektuur (UI, Services/Domäne, infrastruktuur nagu andmejuurdepääs): UI-callbackid peaksid ideaalis tegelema ainult UI-ga. Kõik, mis on „Service“, ei kuulu callback’i. See vähendab reentrancy-efekte märgatavalt.

Seiskamine ilma hangumiseta: „mitte WaitFor, vaid koostööline peatamine“

Lõpetamisel läheb tihti sassi: UI sulgub, lõime peaks lõpetama, kuid järjekorras olevad UI-tööd on veel avatud. Puhtal seiskamisel ei ole eesmärk lõime tapmine, vaid väike koreograafia:

  1. Seiskamislipu seadmine (nt TUiDispatcher.BeginShutdown): alates hetkest ei lisata uusi UI-töid.
  2. Workeri koostööline peatamine: Worker kontrollib tühistamislippu (nt TEvent või TCancellationToken-sarnane) ja lõpetab tsüklid/oodangud.
  3. Ärge blokeerige UI-d: Ärge kasutage põhilõimus tugevat ootesilmust. Kui peate „ootama“, siis ainult sõnumitsükli (message loop) jätkumise juures (või veel parem: vältige seda täielikult, käsitledes lõpetamist callbacki kaudu).
  4. Viimased UI-ära koristused ainult siis, kui aknad/controls on garanteeritult veel olemas. VCL-is on ajastus oluline: hiljemalt kui Handle on kadunud, ei tohi järjekorras olevad tööd enam kontrollidele minna.

See protsess on oluline halduse ja toe jaoks: „Rakendus hangub sulgemisel“ on klassikaline vastuvõetavusprobleem, kuigi funktsionaalselt on kõik korrektselt töödeldud. Määratletud seiskamine säästab siin reaalselt aega.

Silumine: kuidas deadlocki käegakatsutavaks teha (ilma oletamiseta)

Kui see hangub, on keskne küsimus: Kes ootab keda? Mõned lähenemised, mis on olemasolevates projektides end ära on tõestanud:

  • Kõik oote-kohad inventeerida: Täisteksti otsing sõnadele WaitFor, Sleep silmuste sees, TEvent.WaitFor, INFINITE. Paljud probleemid on „varjatud“ ootamised (ka teekides).
  • Lõime olek logis: Logige lõime piirides: „Job startet“, „queued UI“, „UI ausgeführt“, „Job fertig“. Nii näete, kas pealõim queued Jobs üldse töötleb.
  • Message-Loop-kahtluse kontrollimine: Kui hangub ainult modaalsete dialoogide või teatud COM-interaktsioonide korral, on Message Loop sageli kitsaskoht. Siis on eesmärk: vähendada UI-käsitlejate koormust, isoleerida COM-kõned, mitte käivitada pikki operatsioone UI-s.
  • Lockid nähtavaks teha: TCriticalSection/TMonitor puhul tasub debug-build koos „Owner“-metaandmetega (nt lõime-ID Enteri ajal) ja ajamõõtmisega. Nii näete, millist locki pealõim parasjagu hoiab, samal ajal kui Worker ootab UI-d.

Oluline on hoiak: Deadlockid ei ole harva „juhuslikud“. Need on deterministlikud tsüklid, mida harva käivitatakse. Kui olete tsükli korrektselt identifitseerinud, on parandus tavaliselt selge.

Andmete juurdepääsu ja liidese-tööde variandid (FireDAC, REST, failisüsteem)

Eriti FireDAC (või muude DB-juurdepääsude korral) kehtib: ühendus, transaktsioon ja Datasets on praktikas lõime-binditud. Worker-lõim peaks oma DB-konteksti ainuomama. UI-kutsed peaksid piirduma kuvamisega, mitte DB-operatsioonidega. Robustne muster on:

  1. Worker käivitab päringu/REST-kõne, arvutab tulemuse, loob DTO.
  2. Worker postitab DTO kaudu Queue/TUiDispatcher.Post UI-le.
  3. UI võtab DTO vastu ja uuendab Controls (ilma worker-objektidele tagasi pöördumata).

Kui teil on ajalooliselt kujunenud segalahendusi („UI triggert DB, DB-Callback triggert UI“), tasub järk-järguline lahtiliitmine: esmalt isoleerida üleandmispunktid (Dispatcher), seejärel viia seisundid Services/Model-i. See on vähem riskantne kui suur ümbertegemine, kuid vähendab Deadlocke märgatavalt.

Järeldus: Deadlockide vältimine tähendab üleandmiste kontrollimist

TThread ja Synchronize ilma UI-Deadlock’ideta on pigem distsipliin kui üksik tehnika: blokeeringuid minimaalseks viia, lukuread selguse hoidmine, Shutdown defineerimine ja sünkroonsete UI-sõltuvuste vähendamine. Näidatud UI-Dispatcher on legacy-situatsioonides eriti kasulik, kuna ta kasutab vaikimisi Queue‚d, lisades vajalikeks sünkroonseteks üleandmisteks aga Timeout ja selged Shutdown-reeglid.

Rakenduse piirangud jäävad: kui pealõim on püsivalt blokeeritud (raske UI-loogika, modaalsete dialoogide ahelad või COM-STA-kutsed), saab Dispatcher vaid diagnoosida ja kontrollitult katkestada. Püsiv lahendus on UI koormuse vähendamine ja vastutuste eraldamine. Kui vajate selleks abi olemasolevas Delphi-rakenduses – lõimepüünistest kuni järk-järgulise stabiliseerimiseni – saate kavatsuse siia paigutada: arutada projekti või moderniseerimisettevõtmist koos Net-Base-ga.

Fachilises kontekstis mängivad ka Delphi Multithreading ja Synchronize Deadlock oluline roll, kui integratsioonid, andmevood ja edasiarendus peavad puhtalt koos töötama.

Arutada projekti või moderniseerimisettevõtmist koos Net-Base-ga.

Jaga postitust

Jaga seda postitust otse

LinkedIn, X, XING, Facebook, WhatsApp ja e-post on kohe saadaval. Instagrami jaoks valmistame kohe lingi ja lühiteksti ette.

e-post

Instagram avatakse uues vahekaardis. Link ja lühitekst kopeeritakse eelnevalt lõikepuhvrisse.