Od teme magazina do projektne prakse
Povezane stranice usluga i tehnologije za članak
Ko u Delphi želi paralelizirati računski intenzivne ili I/O-opterećene zadatke, brzo naiđe na Parallel Programming Library (PPL) i konkretno na TParallel.For. Efekt je često odmah mjerljiv – dok u istom trenutku ne želite „samo kratko“ ažurirati Progress-UI. Upravo tu nastaju tipični zastoji: naizgled nasumična zamrzavanja sučelja, ProgressBar koji skače unatrag ili potpuni Deadlock čim u debuggeru korak po korak prolazite kroz kod.
U ovom članku radi se o TParallel.For threadsichere Progress-UI: robusnom obrascu koji radi u VCL i FMX, koji uredno grupira UI-azuriranja preko TThread.Queue, uzima u obzir Cancel/Abort i dosljedno zaobilazi najčešće zamke Deadlocka. Fokus je na operativnoj stvarnosti: reproducibilno ponašanje, razumljive odgovornosti i smjernice za debugiranje koje pomažu i kada se greška pojavljuje „samo kod klijenata“.
Warum Progress-UI bei TParallel.For so oft schiefgeht
TParallel.For tipično radi na worker-threadovima iz Delphi-threadpoola. Ti threadovi ne smiju direktno pristupati VCL- ili FMX-kontrolama, jer su UI-frameworki (Message Loop, Window Handles, Rendering) vezani za Main Thread. Čak i naizgled bezopasan ProgressBar.Position := … iz workera može dovesti do neodređenog ponašanja: sporadični AV-ovi, zamrznuti prozori ili „treperava“ ažuriranja.
Očigledno rješenje često je TThread.Synchronize. To rješava thread-sigurnost, ali u paralelnim petljama brzo vodi do drugog problema: 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 kad je riječ samo o starvation/lockstep efektu.
I postoji i prava klasa deadlocka: Main Thread čeka (npr. putem WaitFor, Task.Wait ili posredno kroz blokirajuće pozive) na kraj paralelne operacije, dok worker-threadovi pokušavaju poslati posao u Main Thread koristeći Synchronize ili nepažljivu upotrebu Queue. Rezultat: Main Thread čeka worker, workeri čekaju Main Thread.
TThread.Queue vs. TThread.Synchronize: der praktische Unterschied
Oba mehanizma služe za sigurno izvođenje koda u Main Threadu. Razlika je u semantici čekanja:
- TThread.Synchronize: Pozivajući worker čeka dok Main Thread ne izvrši kod. To je ’sinhrono‘, povećava latencu i klasičan je sastojak za Deadlocke kada je Main Thread upravo blokiran.
- TThread.Queue: Worker stavlja kod samo u red za Main Thread i nastavlja s radom. To je „asynchron“, razdvaja niti i u paralelnim scenarijima je gotovo uvijek bolji podrazumijevani izbor – pod uslovom da se kontrolira frekvencija ažuriranja.
Važno: Queue nije opravdanje za neograničeno korištenje. Ako u svakoj iteraciji petlje pošaljete Queue-azuriranje, preplavit ćete Main-Thread-Queue. Tada UI ne zapinje zbog deadlocka, već zbog gole količine poruka. UI djeluje „zagušeno“, i završetak obrade se odlaže jer još stotine ili hiljade UI-azuriranja slijede.
Rubni slučaj koji zaista boli: čekanje u UI-Thread
U poslovnim aplikacijama često se viđa sljedeći tok: klik na dugme „Start“, UI se deaktivira, ProgressDialog se otvori, zatim se sinkrono „čeka“ dok sve ne završi, pa se potom opet aktivira. Ovaj 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 u OnShow pozove rutinu koja interno čeka.
- Dugme za otkazivanje postavi flag, ali Main Thread ipak ostaje zaglavljen u petlji čekanja.
Ako Worker-Threads u tom vremenu koriste Synchronize, deadlock je praktično zajamčen. S Queue također može doći do zastoja ako Main Thread blokira i ne pumpa poruke – jer se tada i Queue ne obrađuje.
Operativna i pouzdana posljedica je: Main Thread ne smije blokirajuće čekati na paralelnu petlju ako su paralelno potrebna UI-azuriranja. Umjesto toga, obrada se mora ili u potpunosti premjestiti u pozadinski task, ili organizirati „asinkroni završetak“ (Callback/Queued završna akcija) koja će na kraju osloboditi UI.
Jasan pristup: Progress samo agregirano, UI-azuriranja ograničena
Robustan obrazac sastoji se od tri jasno odvojene odgovornosti:
- Worker-Threads obavljaju stvarni posao po elementu/indeksu. Oni prijavljuju samo napredak u thread-sigurnom obliku (brojač, Queue, thread-safe Queue).
- Aggregator (često: Main Thread ili dedikirani timer u UI) iz napretka računa UI-status (pozicija, tekst, ETA) i ažurira kontrolne elemente. Tako izbjegavate 1:1 ažuriranja po iteraciji.
- Završetak (također u Main Threadu): reaktivirati UI, prikazati rezultat, sažeti greške, osloboditi resurse.
Zašto ova podjela tako dobro funkcioniše: Workload može biti visokofrekventan (hiljade 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 obično je samo vizuelna buka i troši CPU-vrijeme u Message Pump.
Sigurno brojanje u nitima: Atomar statt Lock
Za jednostavnu ProgressBar često je dovoljan atomarni brojač. „Atomar“ znači: inkrement i čitanje se događaju bez Race Condition, tipično preko TInterlocked. Time izbjegavate Locks (Critical Sections) u hot path petlje.
Provjerena minimalna ideja:
- Ukupna količina je unaprijed poznata (npr. broj zapisa, datoteka, ID-ova).
- Svaka iteracija atomarno povećava DoneCounter.
- UI-Timer periodično čita taj brojač i postavlja ProgressBar.Position.
Prednost: Nema TThread.Queue po elementu, nema preopterećenja UI-a. Nedostatak: nemate detaljne poruke po elementu (npr. ime datoteke). Za to se može dodati druga, ograničena statusna poruka (vidi sljedeći odlomak).
Statusne poruke bez spama: „letzter Status gewinnt“
Ako želite dodatno prikazati kratak tekst (trenutni element, faza, poruka o grešci), potreban je obrazac koji ne preplavljuje UI pri svakom koraku worker-a. U praksi vrlo dobro funkcioniše „letzter Status gewinnt“:
- Worker zapisuje statusnu informaciju u strukturu sigurnu za niti (npr. atomski zamjenjiv String, ili zaštićeno malim Lock-om).
- UI-Timer periodično preuzima posljednji zabilježeni status u jedan Label.
Time UI ostaje reaktivan i ipak vidite da „nešto se događa“. Ovde je važnija životna dob objekta nego sam String: nemojte prenositi reference na kratkotrajne objekte iz Worker-threadova u UI-thread. Ako prenosite objekte, izričito razjasnite vlasništvo.
TParallel.For threadsichere Progress-UI mit TThread.Queue: ein robustes Muster
Postoje scenariji u kojima UI-Timer sam po sebi nije dovoljan: npr. kada na kraju želite sigurno pokrenuti tačno jedno „Fertig“-ažuriranje, ili kada je UI-ažuriranje složeniji korak (npr. unos u log-prozor, ali drosselt). Tada je TThread.Queue prikladan – ali ne po iteraciji, nego ciljno.
Praktčan pristup je Queue samo za događaje niske frekvencije:
- Start-Event (pripremiti UI, zaključati dugmad)
- Periodične Progress-Events (maks. svake X milisekundi)
- Fehler-Events (opciono sakupljani)
- Done-Event (resetirati UI, prikazati rezultat)
Periodičnost ne postižete preko UI-threada, već već u worker-kontekstu: dozvolite worker-u da tek tada jedno UI-azuriranje queueuje kada je od posljednjeg UI-azuriranja 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“. Skupi proračuni, datotečni I/O ili pristupi bazi podataka ne pripadaju u gequeuovani UI-callback. Callback bi trebao samo čitati stanja i postavljati Controls.
Cancel-Handling: Abbrechen ohne Hänger
U pravim aplikacijama prekid nije opcion. Ključno je: Cancel nije „Kill“, nego kooperativno završavanje. Worker-i moraju redovno provjeravati je li postavljen signal za prekid i onda uredno izaći. U Delphi za to postoje različiti putevi (ovisno o PPL-konstrukciji): vlastiti Volatile-flag, atomarni Boolean, ili koncept otkazivanja preko Tasks (ovisno o Delphi-verziji i strukturi).
Za rad su važna dva pravila:
- Cancel muss schnell sichtbar werden: Provjeravajte zastavicu prekida na odgovarajućim mjestima, ne samo na kraju iteracije, kad iteracija može trajati i nekoliko sekundi.
- Cancel muss aufräumen: Otvoreni handle-i, privremene datoteke, transakcije ili zaključavanja ne smiju ostati otvoreni. To znači: u svakoj Worker-iteraciji su try/finally-blokovi obavezni, kad su resursi uključeni.
S aspekta UI-a Cancel treba samo postaviti signal i dovesti UI u stanje „Stopping…“. Stvarno završavanje i ponovno aktiviranje UI-a trebaju se dogoditi u Done-Eventu, a ne odmah pri kliku.
Izbjegavanje deadlockova: najčešće zamke u praksi
Zamka 1: WaitFor/Task.Wait im Main Thread
Ako je glavna nit blokirana, ne može izvršavati Queue-callbacks i obrađivati poruke. To se ponaša kao deadlock, iako workeri nastavljaju ispravno raditi. Rješenje: izbjegavajte blokirajuće waitove u UI-niti. Umjesto toga završnu akciju obavite preko TThread.Queue ili upravljanjem događajima (npr. Timer provjerava „gotovo“).
Zamka 2: Synchronize innerhalb eines Locks
Klasik: Worker drži Critical Section, pozove Synchronize, a u UI-callbacku je (direktno ili indirektno) ponovno potrebna ista Critical Section. Rezultat: kružno čekanje. Pravilo je jednostavno: Ne predajte UI (Synchronize/Queue) dok držite lock. Ako je lock neophodan, prvo preuzmite sve podatke u lokalne varijable, napustite lock, pa tek onda stavite u Queue.
Zamka 3: UI-Callback triggert Reentrancy
Ponekad samo ažuriranje UI-a nije „bezazleno“: postavljanje Properties može izazvati evente (OnChange, OnResize) koji potom pokrenu logiku koja pristupa stanjima workera. To nije deadlock u užem smislu, ali vodi do teško objašnjivih zastoja i race conditiona. Rješenje: smjestite UI-azuriranja u „tihе“ puteve (privremeno onemogućite evente) ili koristite reentrancy-guardove (npr. atomski guard za fazu ažuriranja).
Zamka 4: Zu viele queued Updates
Čak i bez waitova, UI može „stajati“ ako generirate desetine hiljada queued callbackova. Simptomi: ProgressBar dugo nastavlja prikaz, prozor reagira tromo, CPU glavne niti visoko opterećen. Rješenje: dovedite u red (vremenski prozor), agregirajte (brojač), ili koristite pravu Producer/Consumer-strukturu u kojoj može postojati samo jedno pending UI-azuriranje (Coalescing).
Kad postane složenije: prikupljanje rezultata, objedinjavanje grešaka, garantovanje redoslijeda
TParallel.For je idealan ako su iteracije neovisne. U poslovnom softveru su iteracije često samo „uglavnom“ neovisne: čitaju datoteke, pozivaju REST-API-je, pišu redove u bazu podataka. Tada morate pažljivo isplanirati tri dodatna aspekta:
- Thread-sigurno prikupljanje rezultata: Ili po niti lokalni buffer (na kraju objedinite) ili threadsigurna Queue/Collection. Izbjegavajte zaključavanja na hot-pathu.
- Rukovanje greškama: Izuzeci iz radnih niti moraju se prikupiti. U praksi se pokazalo korisnim: zapamtiti prvi izuzetak i pokrenuti Cancel, ili prikupiti sve izuzetke i na kraju ih zajedno prikazati.
- Redoslijed: Ako izlaz treba stabilan redoslijed (npr. logovi po indexu), paralelna obrada s naknadnim sortiranjem često je jednostavnija nego „sigurno za niti uređeno umetanje“.
Za UI to znači: Ne prikazujte svaku pojedinačnu poruku o grešci odmah. To vodi u pakao modalnih dijaloga. Prikupite greške (npr. listu stringova) i na kraju prikažite sažetak ili izvezivi log.
Debugging: Kako deadlock zaista učiniti vidljivim
Deadlockovi u paralelnom kodu su frustrirajući, jer u debuggeru mogu izgledati drugačije nego u Release buildu. Ipak postoji nekoliko vrlo praktičnih poluga:
Iskoristite prozor niti i stogove poziva
Ako se UI zapne, pogledajte sve niti: Gdje stoji der Main Thread? Čeka li? Je li u petlji poruka? Gdje su radne niti? Ako radne niti zapnu u Synchronize, uzrok je gotovo uvijek „Main Thread blokiran“ ili „Main Thread treba lock“.
Označite Queue-/Synchronize-mjesta
Postavite ciljano logiranje na mjestima prijenosa (prije Queue, u Queue-Callbacku, na kraju iteracije). U radu je to često vrijednije od breakpoints, jer je timing presudan. Pazite da samo logiranje bude sigurno za niti i neblokirajuće (npr. nema UI-log izlaza direktno iz radnih niti).
Mjerite vrijeme posljednjeg ažuriranja
Ako UI „zapne“, može biti da mora obraditi previše ažuriranja. Mjerite zato u Main Threadu koliko puta u sekundi izvršavate UI-azuriranje i koliko dugo to traje. Čim UI-callbackovi trebaju više od nekoliko milisekundi, potrebno je uvesti ograničavanje ili pojednostavljenje.
Kada se TParallel.For s Progress-UI zaista isplati?
Paralelizacija nije cilj sama po sebi. Isplati se posebno kada:
- iteracije su dovoljno velike (milisekunde do sekunde), tako da overhead threadpoola biva zanemaren,
- posao je CPU-intenzivan (Parsing, Kompression, Hashing) ili ima I/O koji se dobro paralelizira (više datoteka, više HTTP-Requests s ograničenjima),
- imate jasnu strategiju za Cancel i greške,
- UI zahtjevi mogu raditi s agregiranim progresom.
Manje se isplati kada je svaka iteracija izuzetno kratka (mikro-operacije) ili kada sve iteracije koriste isti uski grlo (serijska DB-transakcija, globalni lock, jedna datoteka). Tada je često brži zahvat: poboljšati algoritam, obrađivati u paketima, smanjiti pristup podacima ili usko grlo eksplicitno odvojiti.
Praktična kontrolna lista: Kako zadržati stabilnu UI
- Main Thread ne smije biti blokiran: nema čekanja, nema dugih petlji bez message pump.
- Radne niti nikad ne diraju Controls: nema VCL/FMX-pristupa izvan UI-threada.
- UI-azuriranja su ograničena: brojači/timeri ili udružena Queue-azuriranja umjesto po iteraciji.
- Nema poziva Synchronize iznutra iz lockova.
- Cancel je kooperativan, često provjeravan i uredno čisti resurse.
- Izuzeci se prikupljaju i na kraju organizovano obrađuju.
Zaključak: Odvojite preko Queue-a, stabilizirajte agregacijom
Robusno sučelje za prikaz napretka pri TParallel.For ne nastaje jednostavnim „ubacivanjem Synchronize bilo gdje“, već jasnim arhitektonskim principom: workeri rade neovisno, Main Thread ostaje slobodan i obrađuje samo nekoliko brzih UI-Updates. TThread.Queue je u tom kontekstu pravi alat, ako ga koristite ciljano i ograničeno. Za „zagušeno“ ili nestabilno ponašanje gotovo uvijek su odgovorne dvije stvari: Main Thread negdje blokira – ili se ugušuje u previše queued Updates.
Ako jednom pravilno postavite obrazac (Counter/Coalescing, Cancel-Flag, Abschluss-Callback), isplati se primijeniti ga na mnogim mjestima u već razvijenoj Delphi-aplikaciji: Import/Export, provjere podataka, zadaci s datotekama i API-jevima – sve postaje responzivnije, bez da pri svakom Progress-Update stvorite nove deadlockove.
Ako trebate podršku pri stabilizaciji paralelnog koda, pri debugiranju zastoja UI-a ili pri urednoj modernizaciji postojećih Delphi-aplikacija: kontaktirajte nas.
Za ovu tematiku su također važni Delphi Parallel Programming Library i Tthread.queue Vs Synchronize. Članak jasno razvrstava te aspekte i pokazuje na šta treba obratiti pažnju u svakodnevnoj praksi.
Razgovarajte o projektu ili modernizacijskom poduhvatu s Net-Base.
Sljedeći korak
Wenn aus dem Thema ein reales Projekt wird, sollten Architektur, Bestand und Betrieb früh zusammen betrachtet werden.
Pružamo podršku ne samo pri pojedinačnim pitanjima, već i kada iz fragmenata izvornog koda, naslijeđenih sistema ili ideja za portal treba nastati robustan poslovni projekat.
- Postojeće stanje, ciljno stanje i tehnički rizici procjenjuju se zajedno.
- REST, Datenzugriff, Portale und Rollout werden nicht als Spätfolgen verschoben.
- Vi rano vidite koji je put ekonomski i operativno održiv.