Net-Base Časopis

24.07.2026

Delphi: TParallel.For sa sučeljem za napredak sigurnim za niti (TThread.Queue) bez deadlockova

Kako kombinirati Delphi TParallel.For s UI-jem za napredak sigurnim za niti: ažuriranja preko TThread.Queue, čista agregacija, rukovanje otkazivanjem i uobičajene zamke deadlocka pri debugiranju i u radu.

24.07.2026

Od teme magazina do projektne prakse

Povezane stranice usluga i tehnologije za članak

Tko u Delphi želi paralelizirati računski intenzivne ili I/O-opterećene zadatke, brzo će naići na Parallel Programming Library (PPL) i konkretno na TParallel.For. Učinak je često odmah mjerljiv – sve dok u istom trenutku ne pokušate „mal eben“ ažurirati Progress-UI. Upravo tu nastaju tipični zastoji: naizgled slučajna zamrzavanja UI‑a, ProgressBar koji skače unatrag ili potpuni deadlock čim u debuggeru idete korak po korak.

U ovom članku radi se o TParallel.For threadsiguran Progress-UI: robusnom obrascu koji radi u VCL i FMX, uredno grupira UI-azuriranja preko TThread.Queue, uzima u obzir Cancel/Abort i dosljedno izbjegava najčešće zamke deadlocka. Fokus je na operativnoj stvarnosti: reproducibilno ponašanje, jasne odgovornosti i smjernice za debugiranje koje pomažu i kada se pogreška pojavi samo „kod kupaca“.

Zašto Progress-UI kod TParallel.For često ne funkcionira

TParallel.For obično se izvodi na worker-nitima iz Delphi-threadpoola. Te niti ne smiju direktno pristupati VCL- ili FMX-kontrolama, jer su UI-okviri (Message Loop, Window Handles, Rendering) vezani za Main Thread. Čak i naizgled bezopasan ProgressBar.Position := … iz workera može dovesti do nedefiniranog ponašanja: sporadični AV-ovi, zamrznuti prozori ili „trepereća“ ažuriranja.

Očigledno rješenje često je TThread.Synchronize. Time se rješava problem sigurnosti niti, ali u paralelnim petljama brzo se javlja drugi problem: stvara serijsko usko grlo. Svaki worker čeka na UI-Thread, koji je zauzet renderiranjem i obradom Synchronize‑poziva. Pod opterećenjem to izgleda kao deadlock – čak i ako je u pitanju „samo“ starvation/lockstep efekt.

A onda postoji prava klasa deadlocka: Main Thread čeka (npr. putem WaitFor, Task.Wait ili indirektno preko blokirajućih poziva) na završetak paralelne operacije, dok worker-niti pokušavaju poslati posao u Main Thread putem Synchronize ili neugodnog korištenja Queue. Rezultat: Main Thread čeka na workere, workeri čekaju na Main Thread.

TThread.Queue vs. TThread.Synchronize: praktična razlika

Unschärfer Debugger-Blick mit Notizen zu Threads und Queue als Kontext für Queue vs Synchronize
Tijekom debugiranja brzo se vidi čekaju li worker‑niti na Main Thread ili samo stavljaju queued ažuriranja.

Oba mehanizma služe za sigurno izvršavanje koda u Main Threadu. Razlika je u semantici čekanja:

  • TThread.Synchronize: Pozivajuća worker‑nit čeka dok Main Thread ne izvrši kod. To je „sinhrono“, povećava latencije i često je izvor deadlockova kad je Main Thread blokiran.
  • TThread.Queue: Radnik stavlja kod samo u red čekanja za Main Thread i nastavlja raditi. To je „asinkrono“, odvaja threadove i u paralelnim scenarijima gotovo uvijek je bolji zadani izbor – pod uvjetom da kontrolirate frekvenciju ažuriranja.

Važno: Queue nije dozvola bez ograničenja. Ako u svakoj iteraciji petlje pošaljete ažuriranje u Queue, preplavit ćete Main-Thread-Queue. UI se tada možda neće zamrznuti zbog deadlocka, ali hoće zbog gole količine poruka. UI će djelovati „sporo“, a završetak obrade će se odgoditi jer se još stotine ili tisuće UI-ažuriranja izvršavaju u zaostatku.

Der Randfall, der wirklich wehtut: Warten im UI-Thread

U poslovnim aplikacijama često se vidi sljedeći tijek: klik na gumb „Start“, UI se deaktivira, ProgressDialog se otvori, zatim se sinkrono „čeka“ dok sve ne završi, nakon čega se ponovno aktivira. Taj obrazac je srž mnogih deadlockova.

Tipične varijante (ovisno o bazi koda):

  • Main Thread pokreće TParallel.For i potom poziva blokirajuću logiku čekanja (direktno ili indirektno).
  • ProgressDialog u konstruktoru ili OnShow poziva rutinu koja interno čeka.
  • Gumb za otkazivanje postavi flag, ali Main Thread i dalje ostane zaglavljen u petlji čekanja.

Ako Worker-Threadovi u tom vremenu koriste Synchronize, deadlock je praktički zajamčen. S Queue također može doći do zastoja ako je Main Thread blokiran i ne pumpa poruke – jer se tada i Queue neće obraditi.

Operativna posljedica glasi: Main Thread ne smije blokirajuće čekati na paralelnu petlju ako su paralelno potrebna UI- ažuriranja. Umjesto toga obrada se mora ili u potpunosti premjestiti u pozadinski zadatak, ili organizirati „asinkrono završavanje“ (callback/queued završna akcija) koje će na kraju ponovno osloboditi UI.

Sauberer Ansatz: Progress nur aggregiert, UI-Updates gedrosselt

Motiv radnog mjesta s apstrahiranim prikazom napretka i mjerenjem vremena kao simbolom ograničenih UI-Updates
Agregacija i vremensko ograničavanje sprječavaju da UI bude preplavljen previše ažuriranjima.

Robustan obrazac sastoji se od tri jasno odvojene odgovornosti:

  • Worker-Threads obavljaju stvarni posao po elementu/indeksu. Oni prijavljuju samo napredak u threadsigurnom obliku (brojač, Queue, thread-safe Queue).
  • Aggregator (često: Main Thread ili posvećeni timer u UI) iz napretka izračunava UI-status (pozicija, tekst, ETA) i ažurira kontrole. Tako izbjegavate 1:1 ažuriranja po iteraciji.
  • Završetak (također u Main Threadu): reaktivirati UI, prikazati rezultat, sažeti pogreške, osloboditi resurse.

Zašto ta razdvojenost tako dobro funkcionira: Workload može biti visoke frekvencije (tisuće elemenata), ali UI treba samo nekoliko ažuriranja u sekundi. U praksi je dovoljno 5–10 ažuriranja/s, kod vrlo brzih poslova čak 2–4. Sve iznad toga najčešće je samo vizualna buka i troši CPU‑vrijeme u Message Pump.

Sigurno brojanje u više niti: atomarno umjesto Lock

Za jednostavnu ProgressBar često je dovoljan atomarni brojač. „Atomarno“ znači: inkrement i čitanje se događaju bez Race Condition, tipično preko TInterlocked. Time izbjegavate Locks (Critical Sections) u hot pathu petlje.

Provjeren minimalni pristup:

  • Ukupna količina je unaprijed poznata (npr. broj zapisa, datoteka, ID‑eva).
  • Svaka iteracija atomarno poveća DoneCounter.
  • UI‑Timer periodično čita brojač i postavlja ProgressBar.Position.

Prednost: Nema TThread.Queue po elementu, nema preopterećenja UI‑a. Nedostatak: nemate detaljne poruke po elementu (npr. naziv datoteke). Za to se može dodati druga, ograničena statusna poruka (vidi sljedeći odjeljak).

Statusne poruke bez spama: „posljednji status pobjeđuje“

Ako želite dodatno prikazati kratak tekst (trenutni element, faza, poruka o pogrešci), treba također obrazac koji neće pri svakom koraku Workera zagušiti UI. U praksi „posljednji status pobjeđuje“ vrlo dobro funkcionira:

  • Worker upisuje statusnu informaciju u thread‑safe strukturu (npr. atomarno zamjenjiv String, ili zaštićeno malim Lockom).
  • UI‑Timer periodično preuzima posljednji viđeni status u Label.

Tako UI ostaje responzivan, a i dalje vidite da „nešto se događa“. Važno je ovdje manje sam string nego životni vijek: ne prenosite reference na kratkotrajne objekte iz radničkih niti u UI‑nit. Ako prenosite objekte, izričito razjasnite vlasništvo.

TParallel.For: siguran za niti prikaz napretka s TThread.Queue — robustan uzorak

Postoje scenariji u kojima sam UI‑Timer nije dovoljan: npr. kad na kraju želite sigurno izazvati točno jedno „Fertig“‑ažuriranje, ili kad je UI‑ažuriranje složeniji korak (npr. unos u log‑prozor, ali ograničeno). U tim situacijama TThread.Queue je prikladan — ali ne po iteraciji, nego ciljano.

Praktican pristup je red samo za događaje niske frekvencije:

  • Start‑event (priprema UI‑a, onemogućavanje tipki)
  • Periodički progress‑eventi (najviše sv X milisekundi)
  • Pogrešne‑eventi (opcijski prikupljeni)
  • Done‑event (reset UI‑a, prikaz rezultata)

Periodiku ne postižete preko UI‑threada, nego već u kontekstu Workera: dopuštate Workeru da tek tada queue‑a UI‑ažuriranje ako je od zadnjeg UI‑ažuriranja prošlo dovoljno vremena. Za to odgovara monotona vremenska referenca (npr. TickCount) plus atomarni „last update“‑vrijednost.

Važno: samo UI‑ažuriranje mora biti „brzo“. Skupa računanja, datotečni I/O ili pristupi bazi podataka ne spadaju u gequeueni UI‑callback. Callback bi trebao samo pročitati stanja i postaviti kontrole.

Cancel‑Handling: Prekid bez zamrzavanja

U stvarnim aplikacijama otkazivanje nije opcionalno. Ključno je: Cancel nije „Kill“, nego kooperativno zaustavljanje. Workeri moraju redovito provjeravati je li postavljen signal za prekid i tada uredno izaći. U Delphi za to postoji nekoliko pristupa (ovisno o PPL‑konstukciji): vlastiti Volatile‑flag, atomarni Boolean, ili koncept Cancellation preko Tasks (ovisno o verziji i strukturi Delphi).

Za rad su važna dva pravila:

  • Cancel muss schnell sichtbar werden: Provjerite zastavicu za prekid na prikladnim mjestima, ne samo na kraju iteracije, ako iteracija može trajati i nekoliko sekundi.
  • Cancel muss aufräumen: Otvoreni handleovi, privremene datoteke, transakcije ili lockovi ne smiju ostati. To znači: u svakoj iteraciji worker-a su try/finally-blokovi obvezni kada su uključeni resursi.

S korisničkog sučelja Cancel bi trebao samo postaviti signal i staviti UI u stanje „Stopping…“. Sama završna obrada i ponovno aktiviranje UI-a tada se obavljaju u Done-Eventu, ne odmah pri kliku.

Izbjegavanje deadlockova: najčešće zamke u praksi

Diagramm eines zyklischen Wartens zwischen Threads als Visualisierung eines Deadlocks
Deadlocks entstehen oft durch zyklisches Warten: UI-Thread blockiert, Worker warten auf UI-Zugriff.

Zamka 1: WaitFor/Task.Wait u glavnoj niti

Ako glavna nit blokira, ne može izvršavati Queue-callbackove i ne može obrađivati poruke. To izgleda kao deadlock, čak i ako worker-i nastave ispravno. Rješenje: nijedno blokirajuće čekanje u UI-niti. Umjesto toga završna akcija preko TThread.Queue ili upravljanje događajima (npr. timer provjerava „gotovo“).

Zamka 2: Synchronize unutar locka

Klasika: Worker drži Critical Section, zatim poziva Synchronize, a u UI-callbacku se (direktno ili indirektno) ponovno treba ista Critical Section. Rezultat: kružno čekanje. Pravilo je jednostavno: bez predaje UI-ja (Synchronize/Queue) dok se drži lock. Ako je lock nužan, dohvatite sve podatke prvo u lokalne varijable, napustite lock, pa onda queueajte.

Zamka 3: UI-Callback pokreće ponovni ulazak

Ponekad samo ažuriranje UI-a nije „bezopasno“: postavljanje svojstava može pokrenuti evente (OnChange, OnResize) koji zauzvrat pokreću logiku koja pristupa stanju workera. To nije deadlock u strožem smislu, ali dovodi do teško objašnjivih zamrzavanja i race conditiona. Rješenje: smjestite UI-ažuriranja u „tihi“ put (privremeno onemogućite evente) ili koristite zaštitu protiv ponovnog ulaska (npr. atomski guard za fazu ažuriranja).

Zamka 4: Previše queued ažuriranja

Čak i bez čekanja, UI može „stajati“ ako stvarate desetke tisuća queued callbackova. Simptomi: ProgressBar dugo naknadno radi, prozor je trom, CPU glavne niti visok. Rješenje: ograničite (vremenski prozor), agregirajte (brojač) ili koristite pravu Producer/Consumer strukturu u kojoj može postojati samo jedno pending UI-ažuriranje (coalescing).

Kada postane složenije: sakupljanje rezultata, grupiranje grešaka, garantiranje redoslijeda

TParallel.For je idealan kad su iteracije neovisne. U poslovnom softveru su iteracije često samo „uglavnom“ neovisne: čitate datoteke, pozivate REST-APIje, pišete retke u bazu podataka. Tada morate tri dodatne točke pažljivo isplanirati:

  • Thread-sigurno prikupljanje rezultata: Ili po niti lokalni buffer (na kraju spojiti) ili thread-sigurna Queue/Collection. Izbjegavajte lockove u hot pathu.
  • Rukovanje pogreškama: Iznimke iz radnih dretvi moraju se prikupiti. U praksi se pokazalo korisnim: zapamtiti prvu iznimku i pokrenuti Cancel, ili prikupiti sve iznimke i na kraju ih objedinjeno prikazati.
  • Redoslijed: Ako je za izlaz potrebna stabilna poredanost (npr. logovi po indeksu), paralelna obrada s naknadnim sortiranjem često je jednostavnija od „thread-safe uređenog umetanja“.

Za UI to znači: nemojte odmah prikazivati svaku pojedinačnu poruku o pogrešci. To vodi u pakao modalnih dijaloga. Prikupite pogreške (npr. listu stringova) i na kraju prikažite sažetak ili log koji se može izvesti.

Debugging: Kako deadlock doista učiniti vidljivim

Deadlockovi u paralelnom kodu frustriraju, jer u debuggeru mogu izgledati drugačije nego u Release buildu. Ipak, postoji nekoliko vrlo praktičnih poluga:

Iskoristite prozor Threads i Call Stacks

Ako se UI zamrzne, pogledajte sve dretve: gdje stoji Main Thread? Čeka li? Je li u petlji poruka? Gdje su worker-dretve? Ako se workeri zapinju u Synchronize, uzrok je gotovo uvijek „Main Thread blokiran“ ili „Main Thread treba Lock“.

Označavanje Queue-/Synchronize-mjesta

Postavite ciljano logiranje na mjestima predaje (prije Queuea, u Queue-callbacku, na kraju iteracije). U radu je to često vrijednije od breakpoints, jer je tajming presudan. Pazite da samo logiranje bude thread-safe i ne blokira (npr. bez izravnog UI-ispisa logova iz worker-dretvi).

Mjerite vrijeme posljednjeg ažuriranja

Ako se UI „zamrzne“, moguće je da mora obraditi previše ažuriranja. Mjerite stoga u Main Threada koliko često po sekundi izvršavate UI-azuriranje i koliko ono traje. Čim UI-callbackovi trebaju više od nekoliko milisekundi, potrebno je ograničavanje ili pojednostavljenje.

Kada se TParallel.For s Progress-UI doista isplati?

Paralelizacija nije sama sebi svrha. Posebno se isplati kada:

  • iteracije su dovoljno velike (milisekunde do sekunde), tako da Threadpool-Overhead postane zanemariv,
  • zadatak je CPU-intenzivan (parsiranje, kompresija, hashing) ili ima I/O koji se dobro paralelizira (više datoteka, više HTTP-Requests s limitima),
  • imate jasnu strategiju za Cancel i za greške,
  • UI zahtjevi mogu se zadovoljiti agregiranim Progressom.

Manje se isplati ako je svaka iteracija ekstremno kratka (mikro-operacije) ili ako sve iteracije nailaze na isti uski grlo (serijska DB-transakcija, globalni Lock, jedna datoteka). Tada je brži pristup često: poboljšati algoritam, batchati, smanjiti pristup podacima ili eksplicitno odvojiti usko grlo.

Praktična kontrolna lista: Kako UI ostane stabilna

  • Main Thread se ne blokira: nema čekanja, nema dugih petlji bez obrade poruka.
  • Workeri nikada ne smiju dirati Controls: nema VCL/FMX-pristupa izvan UI-threada.
  • UI-azuriranja su ograničena: Counter/Timer ili coalesced Queue-azuriranja umjesto po iteraciji.
  • Ne pozivati Synchronize dok se drži Lock.
  • Cancel je kooperativan, često provjeravan i čisti stanje pri otkazivanju.
  • Iznimke se prikupljaju i na kraju se uredno obrađuju.

Zaključak: Odvojite preko Queuea, stabilizirajte agregacijom

Robusno Progress-UI kod TParallel.For ne nastaje time da se „negdje Synchronize ubaci“, nego jasnim arhitektonskim principom: radnici rade neovisno, Main Thread ostaje slobodan i obrađuje samo nekoliko brzih UI-azuriranja. TThread.Queue je u tom kontekstu odgovarajući alat ako ga koristite ciljano i ograničeno. Za „tvrdo“ ili nestabilno ponašanje gotovo uvijek su odgovorne dvije stvari: Main Thread negdje blokirajuće čeka – ili se uguši u previše queued Updates.

Ako jednom uredno postavite obrazac (Counter/Coalescing, Cancel-Flag, Abschluss-Callback), isplati se primijeniti ga na mnogim mjestima u zreloj Delphi-aplikaciji: Import/Export, provjere podataka, zadaci s datotekama i API-jobs – sve postaje responzivnije, bez da si kod svakog Progress-Updatea uzrokujete nove deadlockove.

Ako trebate podršku pri stabilizaciji paralelnog koda, pri debugiranju UI-zamrzavanja ili pri kvalitetnoj modernizaciji zrelih Delphi-aplikacija: kontaktirajte nas.

Za ovu temu su također važni Delphi Parallel Programming Library i Tthread.queue Vs Synchronize. Članak ove aspekte jasno razjašnjava i pokazuje što je važno u praksi.

Raspravite projekt ili plan modernizacije s Net-Base.

sljedeći korak

Ako se tema pretvori u stvarni projekt, arhitekturu, postojeće sustave i operacije trebalo bi rano zajednički razmotriti.

Podržavamo vas ne samo u pojedinačnim pitanjima, već i kada iz isječaka izvornog koda, naslijeđenih sustava ili ideja za portale treba nastati pouzdan poslovni projekt.

  • Postojeće stanje, ciljna slika i tehnički rizici procjenjuju se zajedno.
  • REST, pristup podacima, portali i rollout neće biti odgođeni kao naknadne posljedice.
  • Rano prepoznajete koji je put ekonomski i operativno održiv.

Podijeli objavu

Izravno proslijedite ovu objavu

LinkedIn, X, XING, Facebook, WhatsApp i e-pošta su odmah dostupni. Za Instagram odmah pripremamo poveznicu i kratak tekst.

E-pošta

Instagram se otvara u novoj kartici. Link i kratki tekst se prethodno kopiraju u međuspremnik.