No žurnāla tēmas līdz projektu praksei
Atbilstošas pakalpojumu un tehniskās lapas rakstam
Kurš vēlas paralelizēt skaitļošanas intensīvus vai I/O-intensīvus darbus saistībā ar Delphi, ātri nonāk pie Parallel Programming Library (PPL) un konkrēti pie TParallel.For. Efekts bieži vien ir tūlītēji izmērāms — līdz brīdim, kad tajā pašā elpas vilcienā mēģina “ātri” atjaunināt progresa lietotāja saskarni. Tieši šeit rodas tipiskie aizkavējumi: šķietami nejaušas UI iesaldēšanas, ProgressBar, kas lec atpakaļ, vai pilnīgs deadlock, tiklīdz debuggerī soļo soli pa solim.
Šajā rakstā runa ir par TParallel.For thread‑drošu progresa lietotāja saskarni: robustu modeli, kas darbojas gan VCL, gan FMX, sakārtoti konsolidē UI atjauninājumus caur TThread.Queue, ņem vērā Cancel/Abort un konsekventi apiet biežākās deadlock lamatas. Fokuss ir uz reālo darbību: reproducējama uzvedība, saprotamas atbildības un debugging padomi, kas palīdz pat tad, ja kļūda parādās tikai “pie klienta”.
Kāpēc progresa lietotāja saskarne ar TParallel.For tik bieži noiet greizi
TParallel.For tipiski darbojas uz darba pavedieniem no Delphi threadpool. Šie pavedieni nedrīkst tieši pieskarties VCL vai FMX kontroles elementiem, jo UI ietvari (message loop, window handles, rendering) ir piesaistīti galvenajam pavedienam. Pat šķietami nekaitīgs izsaukums ProgressBar.Position := … no worker pavediena var novest pie nedefinētas uzvedības: sporādiskiem AV, iesaldētām loga virsmām vai „mirgojošiem” atjauninājumiem.
Acīmredzamā labojuma izvēle bieži ir TThread.Synchronize. Tas gan atrisina thread‑safety, taču paralēlās cilpās ātri rada citu problēmu: tiek izveidots seriāls šaurums. Katrs worker gaida, kamēr galvenais pavediens izpildīs kodu, kurš savukārt ir aizņemts ar renderēšanu un Synchronize izsaukumu apstrādi. Slodzes apstākļos tas izskatās kā deadlock — pat ja faktiski notiek starvation/lockstep efekts.
Un tad pastāv īstā deadlock klase: galvenais pavediens gaida (piem., ar WaitFor, Task.Wait vai netieši, caur bloķējošiem izsaukumiem) paralēlās operācijas beigām, kamēr worker pavedieni mēģina ar Synchronize vai neizdevīgu Queue lietojumu nosūtīt darbu galvenajam pavedienam. Rezultāts: galvenais pavediens gaida worker, worker gaida galveno pavedienu.
TThread.Queue vs. TThread.Synchronize: praktiskā atšķirība
Abas metodes nodrošina kodu drošu izpildi galvenajā pavedienā. Atšķirība ir gaidīšanas semantikā:
- TThread.Synchronize: izsaucējs (worker) gaida, līdz galvenais pavediens izpilda kodu. Tas ir “sinhroni”, palielina latentumu un ir klasiska sastāvdaļa deadlock scenārijos, ja galvenais pavediens ir bloķēts.
- TThread.Queue: Der Worker stellt den Code nur in eine Warteschlange für den Main Thread und läuft weiter. Das ist „asynchron“, entkoppelt Threads und ist in Parallel-Szenarien fast immer die bessere Default-Wahl – sofern man die Update-Frequenz kontrolliert.
Wichtig: Queue ist kein Freifahrtschein. Wenn Sie in jeder Iteration einer Schleife ein Queue-Update schicken, überfluten Sie die Main-Thread-Queue. Dann hängt die UI zwar nicht wegen Deadlock, aber wegen schierer Menge an Nachrichten. Die UI wirkt „zäh“, und das Ende der Verarbeitung verzögert sich, weil noch hunderte oder tausende UI-Updates nachlaufen.
Īpaši problemātiskais gadījums: gaidīšana UI pavedienā
Uzņēmumu lietojumprogrammās bieži sastopams šāds gaitas scenārijs: noklikšķina uz pogas „Start“, UI tiek deaktivēta, ProgressDialog tiek atvērts, pēc tam sinhroni „gaida“, līdz viss pabeidzas, un tikai pēc tam tiek atkārtoti aktivizēta. Šis paraugs ir daudzu deadlocku pamatā.
Tipiskās variācijas (atkarībā no koda bāzes):
- Main Thread startet TParallel.For und ruft danach eine blockierende Wartelogik auf (direkt oder indirekt).
- Ein ProgressDialog ruft im Constructor oder OnShow eine Routine auf, die intern wartet.
- Ein Abbrechen-Button setzt zwar ein Flag, aber der Main Thread bleibt trotzdem in einem Warteloop hängen.
Wenn Worker-Threads in dieser Zeit Synchronize benutzen, ist der Deadlock praktisch garantiert. Mit Queue kann es ebenfalls hängen, wenn der Main Thread blockiert und keine Messages pumpt – denn dann wird auch die Queue nicht abgearbeitet.
Die betriebsfeste Konsequenz lautet: Der Main Thread darf nicht blockierend auf die Parallel-Schleife warten, wenn parallel UI-Updates benötigt werden. Stattdessen muss die Verarbeitung entweder vollständig in einen Hintergrund-Task ausgelagert werden, oder man organisiert ein „asynchrones Ende“ (Callback/Queued noslēguma darbība), das die UI am Schluss wieder freigibt.
Sauberer Ansatz: Progress nur aggregiert, UI-Updates gedrosselt
Ein robustes Muster besteht aus drei klar getrennten Verantwortlichkeiten:
- Worker-Threads veic pašus apstrādes uzdevumus katram elementam/indeksam. Tie ziņo tikai par progresu pavedienu drošā veidā (skaitītājs, Queue, thread-safe Queue).
- Aggregator (oft: Main Thread oder ein dedizierter Timer im UI) aprēķina no progresiem UI statusu (Position, Text, ETA) und aktualisiert vadības elementus. So vermeiden Sie 1:1-Updates pro Iteration.
- Abschluss (ebenfalls im Main Thread): UI atkārtoti aktivizēt, rezultātu parādīt, kļūdas apkopot, resursus atbrīvot.
Kāpēc šī atdalīšana darbojas tik labi: darba slodze var būt augstfrekvences (tūkstošiem elementu), bet UI nepieciešami tikai daži atjauninājumi sekundē. Praktiski pietiek ar 5–10 atjauninājumiem/s, ļoti ātriem darbiem pat 2–4. Vairāk par to parasti ir vienkārši optisks troksnis un patērē CPU laiku ziņojumu apstrādes cilpā.
Pavedienu droša skaitīšana: atomāri, nevis bloķēšana
Vienkāršai ProgressBar bieži pietiek ar atomāru skaitītāju. „Atomārs“ nozīmē: inkrementēšana un lasīšana notiek bez sacīkstes nosacījuma, parasti izmantojot TInterlocked. Tas ļauj izvairīties no bloķēšanas (kritiskajām sadaļām) cilpas karstajā ceļā.
Pārbaudīta minimālā ideja:
- Kopējais apjoms ir zināms iepriekš (piem., ierakstu, failu, ID skaits).
- Katra iterācija atomāri palielina DoneCounter.
- UI‑taimeris periodiski nolasa skaitītāju un iestata ProgressBar.Position.
Priekšrocība: neviens TThread.Queue uz elementu, nav UI pārslogojuma. Trūkums: nav detalizētu paziņojumu par katru elementu (piem., faila nosaukums). To var papildināt ar otro, ierobežotu statusa paziņojumu (skat. nākamo sadaļu).
Statusa ziņojumi bez spama: „pēdējais statuss uzvar“
Ja vēlaties papildus parādīt īsu tekstu (aktuālais elements, posms, kļūdas ziņojums), nepieciešama pieeja, kas nepludinātu UI ar katru Worker soli. Praksē princips „pēdējais statuss uzvar“ darbojas ļoti labi:
- Worker ieraksta statusa informāciju pavedienu drošā struktūrā (piem., atomāri aizstājamā String vai aizsargāta ar nelielu bloķēšanu).
- UI‑taimeris periodiski pārnes pēdējo redzēto statusu uz Label.
Tādējādi UI paliek reaģētspējīga, un varat redzēt, ka kaut kas notiek. Svarīgāks šeit nav pats virknes saturs, bet tās dzīves ilgums: nevelciet atsauces uz īslaicīgiem objektiem no Worker pavedieniem uz UI pavedienu. Ja nododat objektus, skaidri nosakiet to īpašumtiesības.
TParallel.For pavedienu droša Progress-UI ar TThread.Queue: uzticama shēma
Ir scenāriji, kuros UI‑taimeris viens pats nepietiek: piemēram, ja beigās vēlaties garantēt tieši vienu „Pabeigts“ atjauninājumu, vai ja UI‑atjauninājums ir sarežģītāks solis (piem., ieraksts žurnāla logā, bet ierobežots). Tad TThread.Queue ir piemērots — taču ne uz katru iterāciju, bet mērķtiecīgi.
Praktiski izmantojama pieeja ir rinda tikai notikumiem ar zemu frekvenci:
- Start‑notikums (sagatavot UI, atspējot pogas)
- Periodiski progress‑notikumi (maks. ik pēc X milisekundēm)
- Kļūdu notikumi (pēc izvēles apkopoti)
- Done‑notikums (atjaunot UI, parādīt rezultātu)
Periodiku nodrošināt nevajag UI‑pavedienā, bet jau Worker kontekstā: ļaujiet Worker tikai tad ievietot rindā UI atjauninājumu, ja kopš pēdējā UI atjauninājuma ir pagājis pietiekami daudz laika. Tam der monotons laika avots (piem., TickCount) plus atomāra „last update“ vērtība.
Svarīgi: UI‑atjauninājumam pašam jābūt „ātram“. Dārgas aprēķinu operācijas, failu I/O vai datubāzes piekļuve nedrīkst atrasties rindā ievietotajā UI‑callback. Callbackam jāizlasa tikai stāvokļi un jāiestata vadības elementi (Controls).
Atcelšanas apstrāde: pārtraukt bez iestrēgšanas
Reālās lietojumprogrammās atcelšana nav izvēles iespēja. Izšķiroši: atcelšana nav „Kill“, bet kooperatīva izbeigšana. Worker regulāri jāpārbauda, vai atcelšanas signāls ir iestatīts, un tad jāiziet tīri. In Delphi ir vairāki veidi (atkarībā no PPL‑konstrukcijas): atsevišķs Volatile karogs, atomārs Boolean vai atcelšanas koncepts caur Tasks (atkarībā no Delphi versijas un struktūras).
Darbībā svarīgas ir divas noteikmes:
- Atcelšanas signālam jābūt ātri pamanāmam: Pārbaudiet atcelšanas karodziņu nozīmīgās vietās, ne tikai iterācijas beigās, ja iterācija var ilgt vairākas sekundes.
- Atcelšana jāveic ar resursu atbrīvošanu: Atvērtie handle, pagaidu faili, transakcijas vai bloķējumi nedrīkst palikt. Tas nozīmē: katrā Worker-iterācijā ir obligāti try/finally-bloki, ja iesaistīti resursi.
UI pusē atcelšana drīkst tikai iestatīt signālu un novest UI uz „Apturošana…“ stāvokli. Faktiskā pabeigšana un UI atkārtota aktivēšana notiek Done-Event, nevis uzreiz pēc klikšķa.
Deadlocku izvairīšanās: biežākās lamatas praksē
Lamatas 1: WaitFor/Task.Wait im Main Thread
Ja galvenais pavediens ir bloķēts, tas nevar izpildīt Queue-callbacks un apstrādāt ziņas. Tas izskatās pēc deadlocka, pat ja Worker korekti turpina darbu. Risinājums: neizmantojiet blokējošus Wait izsaukumus UI-pavedienā. Tā vietā pabeigšanas darbību veiciet caur TThread.Queue vai ar notikumu vadību (piem., taimeris pārbauda „pabeigts“).
Lamatas 2: Synchronize innerhalb eines Locks
Klasiska situācija: Worker tur Critical Section, pēc tam izsauc Synchronize, un UI-callbackā (tieši vai netieši) atkal nepieciešama tā pati Critical Section. Rezultāts: cirkulāra gaidīšana. Noteikums ir vienkāršs: neveidojiet UI-pāreju (Synchronize/Queue) no turēta lock iekšienes. Ja locks nepieciešams, visus datus vispirms nolādējiet lokālajās mainīgajās, atstājiet lock, un tikai pēc tam izmantojiet Queue.
Lamatas 3: UI-Callback triggert Reentrancy
Reizēm UI-atjauninājums pats nav „nekaitīgs“: īpašību iestatīšana var izsaukt eventus (OnChange, OnResize), kas savukārt palaistu loģiku, kas piekļūst Worker stāvokļiem. Tas nav deadlock šaurākā nozīmē, taču rada grūti izskaidrojamas aizturēšanās un race condition. Risinājums: veiciet UI-atjauninājumus „klusos“ ceļos (pagaidu atspējojot eventus) vai izmantojiet reentrancy‑aizsargus (piem., atomāru guard atjaunināšanas fāzei).
Lamatas 4: Zu viele queued Updates
Pats par sevi bez Wait viss var stāvēt, ja ģenerējat desmitiem tūkstošu queued callbacks. Simptomi: ProgressBar ilgi atpaliek, logs reaģē lēni, galvenā pavediena CPU slodze augsta. Risinājums: regulēt (laika logi), apkopot (skaitītājs), vai īsta Producer/Consumer struktūra, kurā var būt tikai viens gaidošs UI‑atjauninājums (coalescing).
Ja kļūst sarežģītāk: rezultātu apkopošana, kļūdu grupēšana, secību garantēšana
TParallel.For ir ideāls, ja iterācijas ir neatkarīgas. Biznesa programmatūrā iterācijas bieži vien ir tikai „lielā mērā“ neatkarīgas: tās lasa failus, izsauc REST-APIs, raksta datubāzes rindas. Tad jums trīs papildu punktus rūpīgi jāplāno:
- Pavedienu droša rezultātu apkopošana: Vai nu katram pavedienam lokāls buferis (pēc tam apvienot), vai pavedienu droša rinda/kolekcija. Izvairieties no bloķējumiem kritiskajā ceļā.
- Kļūdu apstrāde: Izņēmumi no darba pavedieniem ir jāapkopo. Praksē derīgs piegājiens: atcerēties pirmo izņēmumu un izsaukt Cancel, vai arī apkopt visus izņēmumus un beigās tos parādīt apkopoti.
- Secība: Ja izvadei nepieciešama stabila secība (piem., žurnāli pēc indeksa), paralēra apstrāde ar vēlāk veiktu kārtošanu bieži ir vienkāršāka nekā pavedienu droša, sakārtota ievietošana.
UI kontekstā tas nozīmē: nerādiet katru kļūdas ziņojumu uzreiz. Tas noved pie modālo dialogu elles. Apkopoiet kļūdas (piem., virkņu sarakstā) un beigās parādiet kopsavilkumu vai eksportējamu žurnālu.
Atkļūdošana: Kā deadlock padarīt patiesi redzamu
Deadlock paralērajā kodā ir frustējošs, jo debugerī tas var izskatīties citādi nekā Release versijā. Tomēr ir daži praktiski paņēmieni:
Izmantojiet pavedienu logu un izsaukumu stekus
Ja UI aizķeras, pārbaudiet visus pavedienus: kur atrodas galvenais pavediens? Vai tas gaida? Vai tas ir ziņu cilpā? Kur atrodas darba pavedieni? Ja darba pavedieni iestrēgst Synchronize, cēlonis gandrīz vienmēr ir „galvenais pavediens bloķēts“ vai „galvenajam pavedienam nepieciešams lock“.
Atzīmējiet Queue-/Synchronize pārejas vietas
Novietojiet mērķtiecīgu reģistrēšanu (logging) pārejas vietās (pirms Queue, Queue callback, iterācijas beigās). Ražošanā tas bieži ir vērtīgāk nekā breakpoints, jo izšķirošs ir laikā. Pārliecinieties, ka pats logging ir pavedienu drošs un neblokējošs (piem., neveiciet UI žurnāla izvadīšanu tieši no darba pavedieniem).
Mēriet pēdējā atjauninājuma laiku
Ja UI „aizķeras“, iemesls var būt vienkārši pārāk daudz atjauninājumu. Tādēļ izmēriet galvenajā pavedienā, cik reižu sekundē veicat UI atjauninājumu un cik ilgi tas aizņem. Ja UI callbacki prasa vairāk nekā dažas milisekundes, nepieciešama ierobežošana vai vienkāršošana.
Kad TParallel.For ar Progress-UI patiešām atmaksājas?
Paralelizācija nav pati par sevi mērķis. Tā ir īpaši lietderīga, ja:
- iterācijas ir pietiekami garas (milisekundes līdz sekundes), lai pavedienu baseina režijas izmaksas kļūtu nenozīmīgas,
- darbs ir CPU-intensīvs (parsēšana, kompresija, hashing) vai tam ir labi paralelizējama I/O (vairāki faili, vairāki HTTP pieprasījumi ar ierobežojumiem),
- jums ir skaidra Cancel un kļūdu stratēģija,
- UI prasībām pietiek ar apkopotu progresu.
Tas ir mazāk lietderīgi, ja katra iterācija ir ārkārtīgi īsa (mikrooperācijas) vai ja visas iterācijas nonāk pie viena un tā paša sastrēguma punkta (seriāla DB transakcija, globāls lock, atsevišķs fails). Tad biežāk ātrāks risinājums ir: algoritma uzlabošana, batču izmantošana, datu piekļuves samazināšana vai sastrēguma punkta skaidra atdalīšana.
Praktisks kontrolsaraksts: kā noturēt UI stabilu
- Galvenais pavediens nav bloķēts: nav gaidīšanas, nav garu cilpu bez ziņu apstrādes.
- Darba pavedieni nekad nepieskaras kontrolēm: nav VCL/FMX piekļuves ārpus UI pavediena.
- UI atjauninājumi ir ierobežoti: skaitītājs/taimeris vai sapludinātas Queue-atjaunināšanas, nevis par iterāciju.
- Nav Synchronize izsaukumu no lock iekšienes.
- Cancel ir kooperatīvs, bieži pārbaudīts un rūpīgi atbrīvo resursus.
- Izņēmumi tiek apkoptoti un beigās kārtoti apstrādāti.
Secinājums: Atdaliet ar Queue, stabilizējiet ar agregāciju
Robusta Progress-UI pie TParallel.For neveidojas, ievietojot „Synchronize“ kaut kur pa vidu, bet gan pateicoties skaidram arhitektūras principam: darba pavedieni strādā neatkarīgi, galvenais pavediens paliek brīvs un apstrādā tikai dažus, ātrus UI atjauninājumus. TThread.Queue ir pareizais instruments, ja to izmanto mērķtiecīgi un ierobežoti. Par „lēnu“ vai nestabilu uzvedību gandrīz vienmēr atbild divi iemesli: galvenais pavediens kaut kur bloķēti gaida — vai arī tas grimst pārāk daudzos rindā ieliktajos atjauninājumos.
Ja šo paraugu reiz rūpīgi uzstādīsiet (Counter/Coalescing, Cancel-Flag, Abschluss-Callback), tas atmaksāsies daudzās vietās izaugušā Delphi lietojumprogrammā: importā/eksportā, datu pārbaudēs, failu un API darbu apstrādē — viss kļūs reaģētspējīgāks, neradot jaunus deadlock pie katra progresu atjauninājuma.
Ja nepieciešama palīdzība paralēlā koda stabilizācijā, UI iesaldēšanas atkļūdošanā vai izaugušu Delphi lietojumprogrammu tīrā modernizācijā: sazinieties.
Šajā tēmā svarīgas ir arī Delphi Parallel Programming Library un Tthread.queue Vs Synchronize. Raksts saprotami ierindo šos aspektus un parāda, kas ikdienā ir būtisks.
Nākamais solis
Wenn aus dem Thema ein reales Projekt wird, sollten Architektur, Bestand und Betrieb früh zusammen betrachtet werden.
Mēs atbalstām ne tikai atsevišķu jautājumu risināšanā, bet arī tad, kad no avota koda fragmentiem, mantojuma sistēmu jautājumiem vai portāla idejām jāizveido stabils uzņēmuma līmeņa projekts.
- Esošais stāvoklis, mērķa stāvoklis un tehniskie riski tiek kopīgi vērtēti.
- REST, Datenzugriff, Portale und Rollout werden nicht als Spätfolgen verschoben.
- Jūs laikus redzat, kurš risinājums ir ekonomiski un darbības ziņā dzīvotspējīgs.