Ko u Delphi radi s threadovima, prije ili kasnije naiđe na TThread.Synchronize. I upravo tamo se događaju neugodne stvari: sporadična zamrzavanja, „UI ne reaguje“, naizgled slučajni Deadlocks pri zatvaranju ili pri otvaranju dijaloga. Suština rijetko glasi „Delphi je pokvaren“, već gotovo uvijek predstavlja nepovoljan miks Synchronize, blokirajućih operacija čekanja i UI-Threada koji svoju Message Loop (obrada događaja VCL-a) više ne obrađuje uredno. Ovaj članak pokazuje robusne, u Legacy-Kontext praktične obrasce za TThread und Synchronize ohne UI-Deadlocks – uključujući varijantu s timeout-om, uredno prosljeđivanje grešaka, pravila za gašenje i savjete za debugiranje koji pomažu u stvarnim postojećim aplikacijama.
Warum Deadlocks rund um Synchronize in der Praxis entstehen
Synchronize znači: radni thread stavlja proceduru u red koja se izvršava im Main Thread i tipično čeka dok ta procedura ne završi. U VCL-aplikacijama je Main Thread istovremeno UI-Thread (prozor, kontrole, događaji). Dodatno, u mnogim instalacijama tamo rade COM-objekti u STA-Modell (Single-Threaded Apartment: COM-pozivi moraju se obrađivati u istom threadu), što dodatno pojačava ovisnost o funkcionalnoj Message Loop.
Deadlocks obično nastaju zbog jedne od sljedećih konstelacija:
- WaitFor im Main Thread: UI-Thread čeka na worker (npr.
MyThread.WaitFor), dok worker upravo prekoSynchronizetreba UI-Thread. Oboje čekaju – kraj. - Lock-Inversion: Worker drži lock (npr.
TCriticalSectioniliTMonitor) i pozivaSynchronize. Sinhronizovana UI-procedura pokušava preuzeti isti lock (direktno ili indirektno, često preko logginga/cachea/singletona) – klasični Deadlock. - Shutdown/Destroy: Pri zatvaranju forme nit se zaustavlja dok još postoje
Synchronize-zadaci. Posebno neugodno: sinhronizovani pozivi referenciraju kontrole koje se upravo uništavaju. - Message Loop blockiert: modalni dijalozi, dugotrajne UI-operacije, blokirajući COM-poziv ili handler koji „samo malo“ radi DB/REST drže Main Thread.
Synchronize-zadaci se obrađuju kasno ili uopće ne.
Najvažnija posljedica za arhitekturu i operacije: Synchronize je granica blokiranja. U prilagođenom poslovnom softveru s importima, BDE-Ablosung mit nativer Anbindung-Queries, integracijskim poslovima ili pozadinskim servisima s UI-komponentom, ovu granicu treba svjesno kontrolirati – inače iz „rijetko“ postane „uvijek kad je hitno“.
Grundregel: UI-Thread nie auf Worker warten lassen (wenn Synchronize im Spiel ist)
Ako worker negdje koristi Synchronize, Main Thread ne bi smio ne blokirajuće čekati na taj worker. To zvuči trivijalno, ali u legacy kodu je jedan od najčešćih uzroka, jer se brzo ubaci „čekajmo malo pri zatvaranju“ ili „progress-dijalog čeka završetak“.
Praktične posljedice:
- Ne koristiti pozive
WaitForu UI-Threadu, čim u Workeru postoji put koji koristiSynchronize. - Signalizirati završetak niti putem Event/Callback: UI ostaje responsivan, čisti tek nakon signala.
- UI-azuriranja u principu objavljivati preko
TThread.Queueili Dispatcher-a, tako da Worker ne blokira.
TThread.Queue je često bolja zadana opcija: Worker postavlja posao na Main Thread, nastavlja raditi i ne blokira. To sprječava mnoge deadlockove. Međutim, ne rješava sve rubne slučajeve – na primjer ako u Workeru nužno trebate rezultat koji se generira u Main Threadu (npr. pristup resursu vezanom za UI ili komponenti koja je vezana za nit).
TThread i Synchronize bez UI-deadlockova: model razmišljanja za čiste predaje
Robustan model razmišljanja glasi: Postoji samo nekoliko opravdanih sinhronih predaja u Main Thread. Sve ostalo je stanje, prikaz ili telemetrija – i stoga asinhrono.
Jednostavna kategorizacija pomaže u pregledima i pri stabilizaciji postojećih projekata:
- „Samo prikaz“: indikator napretka, log-zapis, brojač, semafor, omogući/onemogući – uvijek
Queue. - „Predaja stanja“: Worker dostavlja objekt podataka/DTO, UI renderuje –
Queue, ali s copy/immutability (dakle bez zajednički mutiranih struktura). - „UI mora odlučiti“: Samo ovdje trebate sinhronu semantiku (npr. upit korisniku). Tada je stvarno pitanje: Mora li Worker zaista čekati, ili se workflow može preurediti (state machine, otkazivanje posla, nastavak kasnije)?
Upravo je treća kategorija zamka za deadlock: ako Worker čeka rezultat iz UI-a, UI je često primorana čekati na Worker (ili posredno preko lockova). To se pod opterećenjem, pri sporim bazama podataka ili u Remote-Desktop okruženjima događa znatno češće.
Primjer izvornog koda: UI-dispatcher s Queue, opcionalnim timeoutom i čistim shutdownom
Slijedeći obrazac enkapsulira predaje prema UI-u u malu pomoćnu klasu. Dobivate:
- Post: fire-and-forget preko
TThread.Queue(tipično za statusna ažuriranja). - Call: sinhroni poziv s Timeout (neobično, ali koristan u legacy-situacijama), bez direktne upotrebe
Synchronizekao tačke blokade. - Shutdown zaštita: Ne prihvataju se novi UI-jobovi, a queued poslovi provjeravaju flag prije nego što se diraju kontrole.
Tehnička klasifikacija: Koristimo Queue plus TEvent (ein Kernel-Event) za povratnu informaciju. Worker ne čeka na Synchronize, već na Event koji se postavlja u Main Threadu nakon što je queued akcija izvršena. Timeout sprječava „vječno“ zadržavanje ako UI-Thread iz bilo kojeg razloga više ne obrađuje zadatke.
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.Svrha koda i gdje je namjerno „neobičan“
Ovaj obrazac ne zamjenjuje u potpunosti Synchronize, ali čini sinkrone prijenose kontroliranim: Worker ne čeka na mehanizam Synchronize, već na Event. Na taj način možete nametnuti timeout-e, u radu učiniti vidljivim da je UI-thread zapeo, i u fazi gašenja dosljedno odbijati nove UI-poslove.
Neobičan dio nije Event, već odluka da se sinkrona semantika prikaže pomoću Queue + Event. To se isplati upravo kada u postojećim aplikacijama morate postepeno unapređivati stabilnost, bez mogućnosti da svako mjesto koje koristi Synchronize odmah arhitektonski preuredite.
Preduvjeti i zamke
- Vidljivost memorije:
DoneEventpredstavlja tačku sinhronizacije. Zbog toga je čitanjeRaisedObjnakonWaitForkonzistentno. Ipak,RaisedObjtreba ostati lokalno po pozivu (kao ovdje), nikada globalno. - Exception-Handling:
AcquireExceptionObjectsprječava da izuzetak u glavnom Threadu „nestane“. Prilikom ponovnog bacanja u Worker nije identičan stacktrace izvoru, ali poruka o grešci ostaje u Worker-logu i Job može uredno propasti. - Timeout ist Diagnose und Schutz: On „ne popravlja“ blokirani glavni Thread. Ali sprječava da Worker neograničeno vezuje resurse (npr. držeći otvorene BDE-Ablosung mit nativer Anbindung-transakcije), i čini klasu grešaka mjerljivom.
- Shutdown muss früh beginnen:
BeginShutdownpripada centralnoj shutdown-sekvenci (npr. vrlo rano uOnCloseQueryglavnog obrasca). Inače će se još UI-Jobs stavljati u red, dok su prozori već uništeni.
Lock-Strategie: so vermeiden Sie Lock-Inversionen mit UI-Callbacks
Mnogi Deadlockovi ne nastaju zbog WaitFor, nego zbog nejasnog reda lock-ova. Tipičan tok: Worker zaključava „model podataka“, poziva UI-azuriranje putem Synchronize, UI-azuriranje ponovo pristupa „modelu podataka“. To je logično razumljivo, ali tehnički fatalno.
Praktična pravila koja se mogu nametnuti u timovima:
- Ne držite Locks über Thread-Grenzen: Prije nego što Worker nešto stavi u red ili sinhronizira prema UI, poslovni lockovi trebaju biti oslobođeni.
- UI liest Snapshots: UI-Callbacks ne bi trebali gledati „uživo“ u strukture Workera, već prikazivati kopije/snapshote (npr. DTO, Record, jednostavne vrijednosti).
- Logging ist ein Lock-Kandidat: Ako Logging interno koristi Queue, Datei-Lock ili Singleton, može postati dio Deadlocka. UI-Callbacks trebaju držati Logging minimalnim ili pisati preko zasebne, neblokirajuće Log-Pipeline.
Ako već imate Layer-3-arhitekturu (UI, Services/Domäne, infrastruktura poput pristupa podacima): UI-Callbacks idealno smiju raditi samo UI. Sve što je „Service“ ne bi smjelo biti u Callbacku. To značajno smanjuje Reentrancy-effekte.
Shutdown ohne Hänger: „nicht WaitFor, sondern kooperatives Stoppen“
Pri gašenju često zapne: UI se zatvara, nit treba otići, ali queued UI-Jobs su još otvoreni. Čist Shutdown je manje „Thread killen“, a više mala koreografija:
- Shutdown-Flag setzen (npr.
TUiDispatcher.BeginShutdown): Od sada nema više novih UI-Jobs. - Worker kooperativ stoppen: Worker provjerava Cancel-Flag (npr.
TEventiliTCancellationToken-slično) i završava petlje/Waits. - UI nicht blockieren: Nema tvrdih čekajućih petlji u glavnom Threadu. Ako morate „čekati“, onda samo uz nastavak Message Loop (ili bolje: uopće izbjeći tako što završetak obradite putem Callbacka).
- Letzte UI-Aufräumarbeiten samo ako su prozori/Controls zajamčeno još prisutni. U VCL je trenutak važan: najkasnije kad je Handle nestao, queued Jobs više ne smiju ići na Controls.
Ovaj postupak je relevantan za Betrieb und Support: „Aplikacija se zamrzava pri zatvaranju“ je klasičan problem prihvatljivosti, iako je tehnički sve ispravno obrađeno. Definiran Shutdown ovdje štedi stvarno vrijeme.
Debugging: Wie Sie den Deadlock greifbar machen (ohne Rätselraten)
Kad zapne, ključno pitanje je: Ko na koga čeka? Nekoliko pristupa koji su se pokazali u postojećim projektima:
- Inventarisati sva mjesta čekanja: Potraga punog teksta za
WaitFor,Sleepu petljama,TEvent.WaitFor,INFINITE. Mnogi problemi su „skrivena“ čekanja (čak i u bibliotekama). - Stanje thread-a u logu: Logirajte na granicama thread-a: „Job pokrenut“, „UI u redu“, „UI izvršeno“, „Job završen“. Tako vidite da li Main Thread uopće obrađuje queued zadatke.
- Provjerite sumnju na Message Loop: Ako se zatezanja javljaju samo kod modalnih dijaloga ili određenih COM-interakcija, Message Loop često predstavlja usko grlo. Cilj tada: rasteretiti UI-handlere, izolirati COM-pozive, izbjegavati dugotrajne operacije u UI.
- Učinite lockove vidljivim: Kod
TCriticalSection/TMonitorisplati se Debug-Build s „Owner“-metapodacima (npr. Thread-ID pri Enter) i vremenskim mjerenjem. Tako vidite koji lock Main Thread trenutno drži dok Worker čeka na UI.
Važno je stav: Deadlockovi rijetko nastaju „slučajno“. To su deterministički ciklusi koji se samo rijetko pokreću. Ako jednom jasno identificirate ciklus, otklanjanje je obično jasno.
Varijante za pristup podacima i zadatke interfejsa (FireDAC, REST, datotečni sistem)
Pogotovo kod FireDAC (ili drugih DB-pristupa) vrijedi: veza, transakcija i Datasets su u praksi vezani za thread. Worker-thread bi trebao isključivo sam posjedovati svoj DB-kontekst. UI-pozivi trebaju se ograničiti na prikaz, a ne na DB-operacije. Robustan obrazac je:
- Worker izvodi Query/REST-poziv, izračunava rezultat, generira DTO.
- Worker postet DTO via
Queue/TUiDispatcher.Postna UI. - UI preuzima DTO i ažurira kontrole (bez oslanjanja na Worker-objekte).
Ako imate historijski narasle mješovite oblike („UI triggert DB, DB-Callback triggert UI“), isplati se postupno razdvajanje: prvo izolirati točke prijenosa (Dispatcher), zatim premjestiti stanja u Services/Model. To je manje rizično od velikog preuređenja, ali značajno smanjuje deadlockove.
Zaključak: Izbjegavanje deadlockova znači kontrolisati prijenose
TThread und Synchronize ohne UI-Deadlocks je manje pojedinačna tehnika nego disciplina: minimizirati blokade, držati redoslijed lockova čistim, definirati shutdown i smanjiti sinkrone ovisnosti o UI. Prikazani UI-Dispatcher je u legacy-situacijama posebno koristan, jer koristi Queue kao zadano, a za potrebne sinkrone prijenose nadograđuje Timeout i jasna pravila za shutdown.
Ograničenja primjene ostaju: Ako je Main Thread trajno blokiran (zbog teške UI-logike, lanaca modalnih dijaloga ili COM-STA poziva), čak i Dispatcher može samo dijagnosticirati i kontrolisano prekinuti. Održivije rješenje je rasteretiti UI i razdvojiti odgovornosti. Ako za to u postojećoj Delphi-aplikaciji trebate podršku – od zamki threading-a do postupne stabilizacije – možete projekt svrstati ovdje: raspraviti projekt ili modernizacijski zahvat s Net-Base.
U stručnom kontekstu također imaju važnu ulogu Delphi Multithreading i Synchronize Deadlock, kad integracije, tokovi podataka i daljnji razvoj moraju precizno surađivati.