Net-Base Магазин

15.05.2026

TThread и Synchronize без застоја корисничког интерфејса (UI): робусни обрасци за VCL и наслеђени код

Kako pouzdano raditi sa TThread, Synchronize i Queue bez zamrzavanja UI: tipični uzroci deadlocka, praktičan UI-dispatcher obrazac (inkl. Timeout), zaštita pri gašenju, strategije zaključavanja i debug provere za razvijene tokom vremena Delphi aplikacije.

15.05.2026

Ko radi u Delphi sa threadovima, pre ili kasnije dolazi do TThread.Synchronize. Upravo tamo se dešavaju neprijatne stvari: sporadična zamrzavanja, „UI ne reaguje“, naizgled slučajni deadlock-ovi pri zatvaranju ili otvaranju dijaloga. Suština retko kada glasi „Delphi je pokvaren“, već gotovo uvek predstavlja nepovoljan miks Synchronize, blokirajućih čekanja i UI-Thread-a koji svoju petlju poruka (obrada događaja VCL-a) više ne obrađuje čisto. Ovaj članak pokazuje robusne, u kontekstu nasleđenih sistema praktične obrasce za TThread und Synchronize ohne UI-Deadlocks – uključujući varijantu sa timeout-om, uredno prosleđivanje grešaka, pravila za gašenje i savete za debugovanje koji pomažu u stvarnim postojećim aplikacijama.

Zašto se u praksi pojavljuju deadlock-ovi oko Synchronize

Synchronize znači: worker-thread stavlja proceduru u red čekanja koja se izvršava u glavnom threadu i obično čeka dok ta procedura ne završi. U VCL-aplikacijama glavni thread je istovremeno i UI-Thread (prozori, kontrole, događaji). Pored toga, u mnogim instalacijama tamo rade COM objekti u STA-modelu (Single-Threaded Apartment: COM pozive treba obrađivati u istom threadu), što dodatno pojačava zavisnost od ispravno funkcionisane petlje poruka.

Deadlock-ovi obično nastaju zbog jedne od sledećih konstelacija:

  • WaitFor im Main Thread: UI-Thread čeka na worker (npr. MyThread.WaitFor), dok worker upravo preko Synchronize treba UI-Thread. Obojica čekaju – kraj.
  • Lock-Inversion: Worker drži lock (npr. TCriticalSection ili TMonitor) i poziva Synchronize. Sinhronizovana UI-procedura pokušava da uzme isti lock (direktno ili indirektno, često preko logovanja/keša/singltona) – klasičan deadlock.
  • Shutdown/Destroy: Pri zatvaranju forme thread se završava dok još postoje Synchronize-zadaci u redu. Posebno podmuklo: sinhronizovani pozivi referenciraju kontrole koje su upravo u procesu uništavanja.
  • Petlja poruka blokirana: modalni dijalozi, dugotrajne UI-operacije, blokirajući COM-poziv ili handler koji „samo malo“ radi DB/REST drže glavni thread zauzetim. Synchronize-zadaci se obrađuju sa zakašnjenjem ili uopšte ne.

Najvažnija posledica za arhitekturu i operacije: Synchronize je granica blokade. U prilagođenom poslovnom softveru sa uvozima, BDE-zamena sa nativnim povezivanjem-upiti, interfejsnim poslovima ili pozadinskim servisima sa UI-komponentom, ovu granicu treba svesno kontrolisati – inače će se iz „retko“ ubrzo pretvoriti u „uvek onda kada je hitno“.

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

Ako neki worker negde koristi Synchronize, glavni thread ne bi smeo blokirajuće da čeka na tog workera. To zvuči trivijalno, ali u legacy-kodu je jedna od najčešćih grešaka, jer se brzo doda „sačekaj kratko pri zatvaranju“ ili „dialog napretka čeka na kraj“.

Praktične posledice:

  • Не користити WaitFor позиве у UI-ниту, када у worker-у постоји пут који користи Synchronize.
  • Завршетак нити сигнализирати преко Event/Callback-а: UI остаје респонзиван и чишћење се врши тек након пријема сигнала.
  • UI-ажурирања у принципу објављивати преко TThread.Queue или dispatcher-а, како би радне нити не биле блокиране.

TThread.Queue је често боља подразумевана опција: Worker шаље посао на главну нит, наставља да ради и не блокира. То спречава многе deadlock-ове. Међутим, не решава све ивичне случајеве – на пример када у worker-у неопходно треба резултат који се генерише у главној нити (нпр. приступ ресурсу везаном за UI или компоненти која је привезана за нит).

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

Робустан мисаони модел гласи: постоји само неколико легитимних синхроних предаја у главну нит. Све остало је статус, приказ или телеметрија – и зато асинхроно.

Једноставна категоризација помаже при ревизијама и при стабилизацији постојећих пројеката:

  • „Само приказати“: напредак, ред у лог-у, бројач, индикатор стања (семафор), омогућавање/онемогућавање – увек Queue.
  • „Предаја стања“: Worker испоручује објекат података/DTO, UI рендерује – Queue, али са копијом/непроменљивошћу (односно без заједнички мутуираних структура).
  • „UI мора да одлучи“: Тек овде вам треба синхрона семантика (нпр. упит кориснику). Тада је стварно питање: да ли радна нит заиста мора да чека, или се workflow може преуредити (машина стања/State Machine, отказивање посла, наставак касније)?

Посебно трећа категорија је замка за deadlock: ако worker чека на резултат из UI-ја, UI ће лако бити искушена да чека на worker (или индиректно преко закључавања). То под оптерећењем, при спорим базама података или у Remote-Desktop окружењима много брже доводи до проблема.

Source-Schnipsel: UI-Dispatcher mit Queue, optionalem Timeout und sauberem Shutdown

Следећи образац капсулира предаје ка UI-ју у малу помоћну класу. Добијате:

  • Post: Fire-and-forget преко TThread.Queue (типично за ажурирања статуса).
  • Call: синхрони позив са Timeout-ом (неуобичајено, али корисно у legacy ситуацијама), без директног коришћења Synchronize као тачке блокаде.
  • Shutdown-Schutz: Не прихватају се нови UI-јобови, а послови који су у реду проверавају флаг пре него што приступе контролама.

Техничка класификација: користимо Queue плус TEvent (један Kernel-Event) за повратну информацију. Worker не чека на Synchronize, већ на догађај који се подеси у главној нити након што је акција из реда извршена. Timeout спречава „вечито“ заглављивање ако UI-нит из неког разлога више не обрађује.

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.

Сврха кода и где је намерно „неуобично“

Шаблон не замењује у потпуности Synchronize, али он чини синхроне преносе контролисаним: Worker не чека на механизам Synchronize, већ на догађај (Event). Тако можете наметнути тајмауте, у раду учинити видљивим да се UI-нит заглавио и у фази гашења доследно одбити нове UI-задатке.

Необичан део није сам Event, већ одлука да се синхрона семантика прикаже помоћу Queue + Event. То има смисла управо када у постојећим апликацијама постепено морате повећавати стабилност, без потребе да одмах архитектонски мењате сва места која користе Synchronize.

Ограничења и замке

  • Видљивост у меморији: DoneEvent је граница синхронизације. Због тога је читање RaisedObj након WaitFor конзистентно. Ипак, RaisedObj треба да остане локално по позиву (као овде), а не глобално.
  • Rukovanje izuzecima: AcquireExceptionObject sprečava da izuzetak „nestane“ u glavnoj niti. Pri ponovnom bacanju u Worker-u stacktrace nije identičan izvornom, ali poruka o grešci ostaje u worker-logu i zadatak može korektno da završi sa greškom.
  • Timeout je dijagnostika i zaštita: On ne „popravlja“ blokiranu glavnu nit. Ali sprečava da Worker neograničeno drži resurse (npr. BDE-Ablosung mit nativer Anbindung-transakcije otvorene), i čini klasu greške merljivom.
  • Shutdown mora početi rano: BeginShutdown pripada centralnoj shutdown-sekvenci (npr. veoma rano u OnCloseQuery glavnog obrasca). U suprotnom će se još UI-zadaci stavljati u red dok su prozori već uništeni.
  • Strategija zaključavanja: kako izbeći lock-inverzije kod UI-Callbacks

    Mnogi deadlock-ovi ne nastaju zbog WaitFor, već zbog nejasnog reda zaključavanja. Tipičan tok: Worker zaključava „model podataka“, poziva ažuriranje UI putem Synchronize, a UI-ažuriranje ponovo pristupa „modelu podataka“. To je logično razumljivo, ali tehnički fatalno.

    Praktična pravila koja se mogu primeniti u timovima:

    • Ne držite lockove preko granica niti: Pre nego što Worker bilo šta stavi u smeru UI u red ili sinhronizuje, stručni lockovi treba da budu oslobođeni.
    • UI čita snimke stanja: UI-Callbacks ne bi trebalo da gledaju „uživo“ u strukture Workera, već da prikazuju kopije/snimke stanja (npr. DTO, Record, jednostavne vrednosti).
    • Logovanje je kandidat za lock: Ako logovanje interno koristi queue, file-lock ili singleton, može postati deo deadlock-a. UI-Callbacks treba da drže logovanje na minimumu ili da pišu preko odvojene, neblokirajuće log-pipeline.

    Ako već imate Layer-3-arhitekturu (UI, Services/Domäne, infrastruktura kao pristup podacima): UI-Callbacks bi idealno trebalo da rade samo UI. Sve što je „Service“ ne pripada u callback. To značajno smanjuje efekte reentrancy-ja.

    Gašenje bez zaglavljivanja: „ne WaitFor, već kooperativno zaustavljanje“

    Pri zatvaranju često nastane problem: UI se zatvara, nit treba da se ukloni, ali queued UI-zadaci su još otvoreni. Ispravan shutdown je manje „ubijanje niti“, a više mala koreografija:

    1. Postaviti shutdown-flag (npr. TUiDispatcher.BeginShutdown): Od sada više nijedan novi UI-zadatak ne sme biti kreiran.
    2. Kooperativno zaustaviti Workera: Worker proverava cancel-flag (npr. TEvent ili TCancellationToken-slično) i prekida petlje/čekanja.
    3. Ne blokirati UI: Ne koristiti oštre petlje čekanja u glavnoj niti. Ako morate „čekati“, onda to činite samo uz nastavak Message Loop-a (ili još bolje: potpuno izbegnite čekanje tako što ćete završetak obraditi preko callback-a).
    4. Poslednje UI-poslove čišćenja raditi samo ako su prozori/kontrola garantovano još prisutni. U VCL je momenat važan: najkasnije kada handle nestane, queued zadaci više ne smeju pristupati kontrolama.

    Ovaj postupak je relevantan za operacije i podršku: „Aplikacija se zalepljuje pri zatvaranju“ je klasičan problem prihvatljivosti, iako je funkcionalno sve obrađeno korektno. Definisano gašenje ovde realno štedi vreme.

    Debugging: Kako učiniti deadlock opipljivim (bez nagađanja)

    Kada se zaglavi, suštinsko pitanje je: Ko na koga čeka? Nekoliko pristupa koji su se pokazali u postojećim projektima:

    • Inventarisati sva mesta sa čekanjem: Potpuna pretraga za WaitFor, Sleep u petljama, TEvent.WaitFor, INFINITE. Mnogi problemi su „skrivena“ čekanja (čak i u bibliotekama).
    • Stanje niti u logu: Logujte na granicama niti: „Job startet“, „queued UI“, „UI ausgeführt“, „Job fertig“. Tako ćete videti da li glavna nit uopšte obrađuje queued poslove.
    • Proveriti sumnju na Message Loop: Ako se zastoj javlja samo kod modalnih dijaloga ili određenih COM-interakcija, Message Loop često predstavlja usko grlo. Cilj je: rasteretiti UI-handler-e, izolovati COM-pozive, izbegavati dugačke operacije u UI.
    • Učiniti lock-ove vidljivim: Za TCriticalSection/TMonitor isplati se Debug-build sa „Owner“-metapodacima (npr. Thread-ID pri Enter) i vremenskim merenjem. Tako vidite koji lock glavna nit trenutno drži dok radne niti čekaju na UI.

    Važno je držanje: Deadlock-ovi retko nastaju „slučajno“. To su deterministički ciklusi koji se retko pokreću. Kad jednom jasno identifikujete ciklus, otklanjanje je obično jasno.

    Varijante za pristup podacima i poslove za interfejse (FireDAC, REST, fajl-sistem)

    Posebno kod FireDAC (ili drugih DB-pristupa) važi: konekcija, transakcija i Datasets su u praksi vezani za nit. Radna nit treba da poseduje svoj DB-kontekst isključivo sama. UI-pozivi bi trebalo da se ograniče na prikaz, a ne na DB-operacije. Robusan obrazac je:

    1. Radna nit izvršava Query/REST-poziv, izračunava rezultat, kreira DTO.
    2. Radna nit postuje DTO putem Queue/TUiDispatcher.Post prema UI.
    3. UI preuzima DTO i ažurira kontrole (bez oslanjanja na objekte radne niti).

    Ako imate istorijski razvijene mešovite oblike („UI triggert DB, DB-Callback triggert UI“), isplati se postepena dezintegracija: prvo izolovati tačke predaje (Dispatcher), zatim stanje preseliti u servise/model. To je manje rizično od velikog preuređenja, a značajno smanjuje deadlock-ove.

    Zaključak: Izbeći deadlock znači kontrolisati predaje

    TThread i Synchronize bez UI-deadlock-ova je manje pojedinačna tehnika, više disciplina: minimizirati blokade, očuvati uredne redoslede zakljucavanja, definisati shutdown i smanjiti sinhrone zavisnosti prema UI. Prikazani UI-dispatcher je posebno koristan u legacy-situacijama jer koristi Queue kao podrazumevanu opciju, ali za neophodne sinhrone predaje nadograđuje Timeout i jasna pravila za gašenje.

    Ograničenja ostaju: ako je glavna nit trajno blokirana (zbog teške UI-logike, lanaca modalnih dijaloga ili COM-STA-poziva), čak ni dispatcher može samo dijagnostikovati i kontrolisano prekinuti. Održivije rešenje je tada rasteretiti UI i razdvojiti odgovornosti. Ako vam u postojećoj Delphi-aplikaciji treba podrška – od zamki u threadingu do postepenog stabilizovanja – možete projekat svrstati ovde: Projekt oder Modernisierungsvorhaben mit Net-Base besprechen.

    U stručnoj sferi igraju važnu ulogu i Delphi Multithreading i Synchronize Deadlock kada integracije, tokovi podataka i dalji razvoj moraju korektno da saradjuju.

    Projekt oder Modernisierungsvorhaben mit Net-Base besprechen.

    Подели објаву

    Поделите ову објаву директно

    LinkedIn, X, XING, Facebook, WhatsApp и е-пошта су одмах доступни. За Instagram припремамо линк и кратак текст.

    Е-пошта

    Инстаграм се отвара у новој картици. Линк и кратак текст се претходно копирају у међуспремник.