Net-Base Žurnāls

15.05.2026

TThread un Synchronize bez UI-deadlockiem: robusti paraugi VCL un mantojuma kodam

Kā ar TThread, Synchronize un Queue uzticami strādāt, lai UI neiestrēgtu: tipiskie deadlock cēloņi, praktiski izmantojams UI-dispečera modelis (iekļaujot Timeout), Shutdown aizsardzība, Lock-stratēģijas un atkļūdošanas pārbaudes ilgstoši attīstītām Delphi-lietojumprogrammām.

15.05.2026

Ikviens, kurš Delphi izmanto pavedienus, agrāk vai vēlāk nonāk pie TThread.Synchronize. Un tieši tur rodas nepatīkamās lietas: sporādiski sasalumi, “UI nereagē”, it kā nejauši deadlocki programmas aizvēršanas vai dialoga atvēršanas brīdī. Būtība reti ir “Delphi ir bojāts”, gandrīz vienmēr tas ir neveiksmīgs Synchronize, bloķējošu gaidīšanas operāciju un UI-pavediena kombinācijas rezultāts, kurš vairs tīri neapstrādā savu Message Loop (VCL notikumu apstrādi). Šajā rakstā tiek parādīti robusti, legacy kontekstā praktiski pielietojami modeļi TThread un Synchronize bez UI-deadlockiem – ieskaitot laika pārrāvuma (Timeout) variantu, korektu kļūdu nodošanu, izslēgšanas noteikumus un debug norādījumus, kas palīdz reālās esošajās lietojumprogrammās.

Kāpēc praksē rodas deadlocki ap Synchronize

Synchronize nozīmē: darba pavediens ievieto procedūru rindā, kura tiek izpildīta galvenajā pavedienā, un parasti gaida, līdz šī procedūra ir pabeigta. VCL lietojumprogrammās galvenais pavediens vienlaikus ir UI-pavediens (logi, kontroles, notikumi). Turklāt daudzās instalācijās tur darbojas COM-objekti STA-Modell (Single-Threaded Apartment: COM izsaukumi jāapstrādā tajā pašā pavedienā), kas vēl vairāk pastiprina atkarību no funkcionējošas Message Loop.

Deadlocki parasti rodas kādā no šādām konstelācijām:

  • WaitFor im Main Thread: UI-pavediens gaida darba pavedienu (piem., MyThread.WaitFor), kamēr darba pavediens tieši tagad, izmantojot Synchronize, vajag UI-pavedienu. Abi gaida – beigas.
  • Lock-Inversion: Darba pavediens tur lock (piem., TCriticalSection vai TMonitor) un izsauc Synchronize. Sinhronizētā UI-procedūra mēģina iegūt to pašu lock (tieši vai netieši, bieži caur logging/cache/singletoniem) – klasiskā deadlock situācija.
  • Shutdown/Destroy: Formas aizvēršanas laikā tiek pabeigts threads, kamēr vēl pastāv Synchronize uzdevumi. Īpaši problemātiski: sinhronizētie izsaukumi atsaucas uz kontrolēm, kas tiek tikko iznīcinātas.
  • Message Loop blockiert: Modāli dialogi, ilgstošas UI-operācijas, bloķējošs COM-izsaukums vai apstrādātājs, kas “ātri” veic DB/REST, notur galveno pavedienu. Synchronize uzdevumi tiek apstrādāti novēlotā secībā vai vispār netiek apstrādāti.

Galvenā konsekvence arhitektūrai un ekspluatācijai: Synchronize ir bloķēšanas robeža. Individuālajā uzņēmumu programmatūrā ar importiem, BDE-aizvietošana ar natīvu pieslēgumu-vaicājumiem, saskarnes darbiem vai fona pakalpojumiem ar UI-komponenti šo robežu jākontrolē apzināti – citādi no “reti” drīz kļūs “vienmēr tad, kad steigā”.

Pamata noteikums: UI-pavediens nedrīkst gaidīt darba pavedienu (ja tiek izmantots Synchronize)

Ja darba pavediens kaut kur izmanto Synchronize, galvenajam pavedienam nevajadzētu stingri bloķējoši gaidīt šo darba pavedienu. Tas izklausās pašsaprotami, taču legacy kodā tas ir viens no izplatītākajiem cēloņiem, jo ātri tiek pievienotas pieejas kā “uzgaidīsim nedaudz pie slēgšanas” vai “progress dialog gaida līdz beigām”.

Praktiskas sekas:

  • Nebūtu WaitFor-izsaukumu UI-pavedienā, tiklīdz Worker pusē pastāv ceļš, kas izmanto Synchronize.
  • Thread noslēgumu signalizēt ar Event/Callback: UI paliek responsīva, sakārto tikai pēc signāla.
  • UI-aktualizācijas pamatā publicēt, izmantojot TThread.Queue vai Dispatcher, lai Worker netiktu bloķēts.

TThread.Queue bieži ir labāka noklusējuma opcija: Worker nosūta darbu galvenajam pavedienam, turpina darboties un neblokkē. Tas novērš daudz deadlocku. Tomēr tas neatrisina visus ārkārtas gadījumus — piemēram, ja Worker obligāti nepieciešams rezultāts, kas tiek radīts galvenajā pavedienā (piem., piekļuve ar UI saistītam resursam vai komponentei, kas ir pavedienam piesaistīta).

TThread un Synchronize bez UI-deadlockiem: domāšanas modelis tīrām nodošanām

Uzticams domāšanas modelis ir: galvenajam pavedienam ir tikai dažas leģitīmas sinhronas nodošanas. Viss pārējais ir stāvoklis, attēlošana vai telemetrija — tātad asinhroni.

Vienkārša iedalījuma palīdz pārskatīšanā un esošu projektu stabilizēšanā:

  • „Tikai parādīt”: progress, žurnāla rinda, skaitītājs, indikators (Ampel), iespējošana/atspējošana — vienmēr Queue.
  • „Stāvokļa nodošana”: Worker piegādā datu objektu/DTO, UI renderē — Queue, bet ar kopēšanu/nenemaināmību (t.i., nekādas kopīgi mutētas struktūras).
  • „UI jāizlemj”: Tik šeit nepieciešama sinhrona semantika (piem., lietotāja apstiprinājums). Tad īstais jautājums ir: vai Worker tiešām ir jāgaida, vai arī darba plūsma var tikt pārveidota (State Machine, darbu atcelt, vēlāk turpināt)?

Tieši trešā kategorija ir deadlock slazds: ja Worker gaida UI rezultātu, UI ātri tiek vilināta gaidīt Worker (vai netieši — ar bloķēšanas mehānismiem). Tas izraisa problēmas slodzes apstākļos, pie lēnām datubāzēm vai Remote-Desktop vidēs.

Koda fragments: UI-Dispatcher ar Queue, izvēles Timeoutu un tīru Shutdown

Nākamais paraugs kapsulē UI-nodošanas mazā palīgklasē. Jūs iegūsiet:

  • Post: fire-and-forget, izmantojot TThread.Queue (tipiski statusa atjauninājumiem).
  • Call: sinhrons izsaukums ar Timeout (neparasti, bet noderīgi Legacy situācijās), neveicot tiešu Synchronize kā bloķēšanas punktu.
  • Shutdown-Schutz: vairs nepieņemt jaunus UI-uzdevumus, un rindā esošie uzdevumi pārbauda karogu pirms Controls piekļuves.

Tehniska klasifikācija: mēs izmantojam Queue plus TEvent (ein Kernel-Event) atgriezeniskai saitei. Worker negaida Synchronize, bet gan Event, kuru galvenajā pavedienā iestata pēc tam, kad queued Action tika izpildīta. Timeout novērš «mūžīgu» iestrēgšanu, ja UI-pavedienam kāda iemesla dēļ vairs nav iespējas apstrādāt.

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.

Koda mērķis un kur tas apzināti ir „neparasts“

Šis paraugs neaizstāj Synchronize pilnībā, taču tas padara sinhronus pārejas kontrolējamus: darba pavediens vairs negaida uz Synchronize mehānismu, bet gan uz notikumu. Tas ļauj uzspiest laika limitus (timeouts), ekspluatācijā parādīt, ka UI pavediens blokējas, un izslēgšanas fāzē konsekventi noraidīt jaunus UI darbus.

„Neparastā“ daļa nav pats notikums, bet lēmums attēlot sinhrono semantiku ar Queue + Event. Tas ir pamatoti tieši tad, kad jums esošajās lietojumprogrammās jāuzlabo stabilitāte pa posmiem, bez nepieciešamības tūlīt arhitektoniski pārveidot katru Synchronize vietu.

Nosacījumi un bīstamie punkti

  • Atmiņas redzamība: DoneEvent ir sinhronizācijas robeža. Tādējādi lasīšana no RaisedObj pēc WaitFor ir konsekventa. Tomēr RaisedObj jāpaliek lokālam katram izsaukumam (kā šeit), nekad globālam.
  • Exception-Handling: AcquireExceptionObject novērš to, ka izņēmums pazūd galvenajā pavedienā. Pārmetot to atkārtoti darba pavedienā, stacktrace nebūs identisks izcelsmei, taču kļūdas ziņojums paliek darba pavediena žurnālā, un uzdevums var korekti neizdoties.
  • Timeout ist Diagnose und Schutz: Tas nepieārstē bloķētu galveno pavedienu. Tas tomēr novērš, ka darba pavedieni bezgalīgi saista resursus (piem., atstājot atvērtas BDE-Ablosung mit nativer Anbindung-transakcijas), un tas padara kļūdu klasi izmērāmu.
  • Shutdown muss früh beginnen: BeginShutdown jāievieto centrālā shutdown-sekvencē (piem., ļoti agri OnCloseQuery galvenajā formā). Pretējā gadījumā vēl tiek queued UI-uzdevumi, kamēr logi jau ir iznīcināti.
  • Lock-Strategie: so vermeiden Sie Lock-Inversionen mit UI-Callbacks

    Daudzi deadlocki rodas nevis dēļ WaitFor, bet gan neskaidras lock-secības. Tipisks scenārijs: darba pavediens aizslēdz „datu modeli“, izsauc UI-atjauninājumu ar Synchronize, UI-atjauninājums atkal piekļūst „datu modelim“. Tas ir loģiski saprotami, bet tehniski fatāli.

    Praktiskas vadlīnijas, kuras komandas var ievērot:

    • Nepaturēt lockus pāri pavedienu robežām: Pirms darba pavediens ieraksta/sinhronizē kaut ko uz UI, jābūt atbrīvotiem funkcionāliem lockiem.
    • UI liest Snapshots: UI-callbackiem nevajadzētu skatīties “live” darba pavediena struktūrās; jāparāda kopijas/snapshoti (piem., DTO, Record, vienkāršas vērtības).
    • Logging ist ein Lock-Kandidat: Ja logging iekšēji izmanto rindu, faila locku vai singleton, tas var kļūt par deadlock daļu. UI-callbackiem jāierobežo logging līdz minimumam vai jāraksta caur atsevišķu, neblokējošu log-pipeline.

    Ja jums jau ir Layer-3-arhitektūra (UI, Services/Domäne, infrastruktūra kā datu piekļuve): UI-callbackiem ideālā gadījumā jāveic tikai UI funkcijas. Viss, kas ir “Service”, nepieder callbackam. Tas būtiski samazina reentrancy efektus.

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

    Aizveroties bieži iestājas problēma: UI aizveras, pavedienam jāpamet, bet rindā esošie UI-uzdevumi vēl ir atvērti. Kārtīgs shutdown nav “pavediena nogalināšana”, bet neliela horeogrāfija:

    1. Shutdown-Flag setzen (piem., TUiDispatcher.BeginShutdown): No šī brīža vairs nav jaunu UI-uzdevumu.
    2. Worker kooperativ stoppen: Darba pavediens pārbauda cancel-flag (piem., TEvent vai TCancellationToken-līdzīgu) un pabeidz cilpas/gaidīšanas.
    3. UI nicht blockieren: Nav cietas gaidīšanas cilpas galvenajā pavedienā. Ja jāgaida, tad tikai ar darbības ziņu ciklu (vai vēl labāk: pilnībā izvairīties, apstrādājot pabeigšanu caur callback).
    4. Letzte UI-Aufräumarbeiten tikai tad, ja logi/kontroles garantēti vēl pastāv. VCL kontekstā laiks ir svarīgs: ne vēlāk kā brīdī, kad Handle vairs nav, rindā esošiem uzdevumiem nedrīkst piekļūt kontroles elementiem.

    Šī secība ir svarīga ekspluatācijai un atbalstam: “Lietotne karājas aizvēršanas laikā” ir klasiska pieņemšanas problēma, lai gan funkcionāli viss var būt apstrādāts pareizi. Definēts shutdown šeit reāli ietaupa laiku.

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

    Ja tas iestrēgst, pamatjautājums ir: Wer wartet auf wen? Dažas pieejas, kas sevi pierādījušas esošajos projektos:

    • Visas gaidīšanas vietas inventarizēt: pilnteksta meklēšana pēc WaitFor, Sleep ciklos, TEvent.WaitFor, INFINITE. Daudzas problēmas ir „slēptas“ gaidīšanas (arī bibliotēkās).
    • Pavediens — stāvoklis žurnālā: reģistrējiet žurnālā pavedienu robežās: „Job startet“, „queued UI“, „UI ausgeführt“, „Job fertig“. Tā jūs redzēsiet, vai galvenais pavediens vispār apstrādā ierindotos darbus.
    • Pārbaudīt aizdomas par ziņojumu cilpu: ja iesaldēšana parādās tikai modālajos dialogos vai noteiktās COM mijiedarbībās, bieži vainīgs ir ziņojumu cilpa. Mērķis ir: atvieglot UI apstrādātājus, izolēt COM izsaukumus un neveikt garas operācijas UI pavedienā.
    • Bloķējumus padarīt redzamus: pie TCriticalSection/TMonitor ir vērts izmantot debug-būvi ar „Owner“-metadatiem (piem., pavediena ID Enter brīdī) un laika mērījumiem. Tādējādi redzēsiet, kuru bloķējumu galvenais pavediens tur tajā brīdī, kamēr darba pavedieni gaida uz UI.

    Svarīga ir pieeja: Deadlocki reti ir „nejauši“. Tie ir deterministiskas cilpas, kuras tikai reti tiek izraisītas. Kad esat šo cilpu skaidri identificējis, tās novēršana parasti ir skaidra.

    Varianti datu piekļuvei un saskarnes darbiem (FireDAC, REST, Dateisystem)

    Īpaši pie FireDAC (vai citiem DB piekļuves slāņiem) jāsaprot: savienojums, transakcija un Datasets praksē ir pavedienam piesaistīti. Darba pavediens nedrīkst dalīt savu DB kontekstu — tam jābūt ekskluzīvam. UI izsaukumiem jāierobežojas pie attēlošanas, nevis DB operācijām. Robusts raksturlīkums ir:

    1. Darba pavediens izpilda Query/REST-izsaukumu, aprēķina rezultātu, ģenerē DTO.
    2. Darba pavediens nosūta DTO caur Queue/TUiDispatcher.Post uz UI.
    3. UI pieņem DTO un atjaunina vadības elementus (bez atsauces uz darba pavediņa objektiem).

    Ja jums ir vēsturiskas jauktas formas („UI triggert DB, DB-Callback triggert UI“), ir jēga veikt pakāpenisku atslēgšanu: vispirms izolēt nodošanas punktus (Dispatcher), pēc tam stāvokļus pārvietot uz servisiem/modeli. Tas ir mazāk riskanti nekā liels pārbūves projekts, bet ievērojami samazina deadlockus.

    Secinājums: Deadlocku izvairīšanās nozīmē kontroli pār pārejām

    TThread und Synchronize ohne UI-Deadlocks nav vienkārši viena tehnika, bet disciplīna: minimizēt bloķēšanas, uzturēt skaidru bloķējumu secību, definēt Shutdown un samazināt sinhronās UI atkarības. Rādītais UI-Dispatcher ir īpaši noderīgs legacy-situācijās, jo tas izmanto Queue kā noklusējumu, bet nepieciešamām sinhronām pārejām pievieno Timeout un skaidras Shutdown-izstrādnes.

    Izpildes ierobežojumi paliek: ja galvenais pavediens pastāvīgi tiek bloķēts (sakarā ar smagu UI loģiku, modālu dialogu ķēdēm vai COM-STA izsaukumiem), arī Dispatcher var tikai diagnosticēt un kontrolēti pārtraukt darbību. Ilgtermiņa risinājums ir atvieglot UI un skaidri sadalīt atbildības. Ja jums nepieciešama atbalsta palīdzība esošā Delphi lietotnē — no threading-ķermeņiem līdz pakāpeniskai stabilizācijai — varat šo pasākumu klasificēt šeit: Projektu vai modernizācijas darbu apspriest ar Net-Base.

    Fakts: arī Delphi multithreading un Synchronize-Deadlock spēlē svarīgu lomu, ja integrācijas, datu plūsmas un turpmākā attīstība jāvirza kopā ar augstu precizitāti.

    Projektu vai modernizācijas darbu apspriest ar Net-Base.

    Kopīgot ierakstu

    Kopīgot šo ierakstu tieši

    LinkedIn, X, XING, Facebook, WhatsApp un e-pasts ir uzreiz pieejami. Instagramam saiti un īsu tekstu sagatavosim nekavējoties.

    E-pasts

    Instagram atveras jaunā cilnē. Saite un īss teksts tiek iepriekš nokopēti starpliktuvē.