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 prekoSynchronizetreba UI-Thread. Obojica čekaju – kraj. - Lock-Inversion: Worker drži lock (npr.
TCriticalSectioniliTMonitor) i pozivaSynchronize. 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-нит из неког разлога више не обрађује.
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треба да остане локално по позиву (као овде), а не глобално.
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.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:
- Postaviti shutdown-flag (npr.
TUiDispatcher.BeginShutdown): Od sada više nijedan novi UI-zadatak ne sme biti kreiran. - Kooperativno zaustaviti Workera: Worker proverava cancel-flag (npr.
TEventiliTCancellationToken-slično) i prekida petlje/čekanja. - 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).
- 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,Sleepu 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/TMonitorisplati 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:
- Radna nit izvršava Query/REST-poziv, izračunava rezultat, kreira DTO.
- Radna nit postuje DTO putem
Queue/TUiDispatcher.Postprema UI. - 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.