Net-Base Lehti

24.07.2026

Delphi: TParallel.For säieturvallisella edistymis‑käyttöliittymällä (TThread.Queue) ilman lukkiutumisia

Näin yhdistät Delphi TParallel.Forin säikeiturvallisen Progress-UI:n kanssa: päivitykset TThread.Queue:n kautta, siisti aggregointi, peruutuskäsittely ja tyypilliset deadlock-ansat debuggauksessa ja tuotantoympäristössä.

24.07.2026

Lehden aiheesta projektikäytäntöön

Artikkeliin liittyvät palvelu- ja tekniikkasivut

Jos haluaa Delphi rinnakkaistaa laskennallisesti raskaita tai I/O-kuormitettuja töitä, päätyy nopeasti Parallel Programming Libraryn (PPL) pariin ja konkreettisesti TParallel.For-kutsuun. Vaikutus on usein heti mitattavissa – kunnes saman tien yritetään “vain” päivittää Progress-UI. Tässä syntyvät tyypilliset jumit: näennäisesti satunnaiset käyttöliittymän jäätymät, ProgressBar, joka hyppää taaksepäin, tai täydellinen deadlock, kun debuggaaja etenee askel kerrallaan.

Tässä artikkelissa käsitellään TParallel.For:n säikeiturvallista Progress-UI:ta: vankka malli, joka toimii sekä VCL:ssä että FMX:ssä, kokoaa UI-päivitykset siististi TThread.Queue -kutsujen taakse, huomioi Cancel/Abort-tilanteet ja kiertää johdonmukaisesti yleisimmät deadlock-kalat. Painopiste on käyttötilanteissa: toistettava käyttäytyminen, selkeät vastuualueet ja debuggausvinkit, jotka auttavat myös silloin kun virhe ilmenee “vain asiakkaalla”.

Miksi Progress-UI TParallel.For:ssa menee niin usein pieleen

TParallel.For ajaa tyypillisesti worker-säikeillä, jotka tulevat Delphi-threadpoolista. Nämä säikeet eivät saa käsitellä VCL- tai FMX-ohjaimia suoraan, koska UI-kehykset ovat sidottuja Main Threadiin (Message Loop, Window Handles, Rendering). Jo näennäisen harmiton ProgressBar.Position := … worker-säikeestä voi johtaa määrittelemättömään käyttäytymiseen: satunnaisiin AV-virheisiin, ikkunoiden jäätymiseen tai „välkkyviin“ päivityksiin.

Ilmeinen korjaus on usein TThread.Synchronize. Se kyllä varmistaa säikeiturvallisuuden, mutta rinnakkaisissa silmukoissa se johtaa nopeasti toiseen ongelmaan: se luo seriaalisen pullonkaulan. Jokainen worker odottaa Main Threadin suorittavan koodin, ja Main Thread puolestaan on varattu renderöintiin ja Synchronize-kutsujen käsittelyyn. Kuormituksen alla tilanne voi muistuttaa deadlockia – vaikka kyse olisi vain starvation-/lockstep-ilmiöstä.

On myös aidon deadlockin luokka: Main Thread odottaa (esim. WaitFor:lla, Task.Wait:lla tai epäsuorasti estävien kutsujen kautta) rinnakkaisoperaation päättymistä, samalla kun worker-säikeet yrittävät lähettää töitä Main Threadille Synchronize:n tai epäedullisen Queue-käytön kautta. Tulos: Main Thread odottaa workereita, workerit odottavat Main Threadai.

TThread.Queue vs. TThread.Synchronize: der praktische Unterschied

Unschärfer Debugger-Blick mit Notizen zu Threads und Queue als Kontext für Queue vs Synchronize
Debuggaamisen yhteydessä näkee nopeasti, odottavatko worker-säikeet Main Threadiä vai asettavatko ne pelkästään queued-päivityksiä.

Molemmat mekanismit mahdollistavat koodin turvallisen suorittamisen Main Threadissä. Erot tulevat esiin odotussemantiikassa:

  • TThread.Synchronize: Kutsuva worker odottaa, kunnes Main Thread on suorittanut koodin. Tämä on „synkronista“, lisää latenssia ja on klassinen ainesosa deadlockien syntyyn, jos Main Thread on juuri estetty.
  • TThread.Queue: Työsäie asettaa koodin vain jonoon pääsäikeelle ja jatkaa suoritustaan. Tämä on „asynkronista“, irrottaa säikeet toisistaan ja on rinnakkaisskenaarioissa lähes aina parempi oletusvalinta – edellyttäen, että päivitystaajuutta hallitaan.

Tärkeää: Queue ei ole vapaa passi. Jos lähetät Queue-päivityksen jokaisessa silmukan iteraatiossa, tulvitat pääsäikeen jonon. Silloin käyttöliittymä ei jumitu deadlockin vuoksi, mutta viestien määrän takia. Käyttöliittymä vaikuttaa „jäykältä“, ja käsittelyn päättyminen viivästyy, koska vielä sadat tai tuhannet käyttöliittymäpäivitykset suoritetaan perässä.

Ääritapaus, joka todella sattuu: odottaminen käyttöliittymäsäikeessä

Yrityssovelluksissa nähdään usein seuraava kulku: painiketta „Start“ painetaan, käyttöliittymä deaktivoidaan, ProgressDialog avautuu, sitten synkronisesti „odotetaan“, kunnes kaikki on valmis, ja lopuksi aktivoidaan uudelleen. Tämä malli on monien deadlockien ydin.

Tyypillisiä muunnelmia (koodipohjasta riippuen):

  • Pääsäie käynnistää TParallel.For ja kutsuu sen jälkeen blokkaavan odotuslogiikan (suoraan tai epäsuorasti).
  • ProgressDialog kutsuu Constructor- tai OnShow-kohdassa rutiinia, joka odottaa sisäisesti.
  • Peruuta-painike asettaa kyllä lipun, mutta pääsäie jää silti odotussilmukkaan.

Jos työsäikeet tämän aikana käyttävät Synchronize-kutsua, deadlock on käytännössä taattu. Myös Queue voi jumiuttaa, jos pääsäie on blokattu eikä pumppaa viestejä – koska silloin myös jonoa ei käsitellä.

Käytännön seuraamus on: Pääsäie ei saa estävästi odottaa rinnakkaissilmukkaa, jos rinnakkain tarvitaan käyttöliittymäpäivityksiä. Sen sijaan käsittely on joko siirrettävä kokonaan taustatehtävään tai järjestettävä „asynkroninen loppu“ (callback/jonotettu päättämistoiminto), joka vapauttaa käyttöliittymän lopuksi.

Siisti lähestymistapa: edistymisraportointi aggregoituna, käyttöliittymäpäivitykset rajoitettuina

Työpistekuva, jossa abstraktoitu edistymän näyttö ja ajanseuranta symboloivat rajoitettuja käyttöliittymäpäivityksiä
Aggregointi ja aikaohjattu rajoitus estävät, että käyttöliittymä tulvii liian monista päivityksistä.

Vakaa malli koostuu kolmesta selkeästi erillisestä vastuusta:

  • Työsäikeet suorittavat varsinaisen työn kutakin elementtiä/indeksiä kohden. Ne raportoivat vain edistymän säikeiturvallisessa muodossa (laskuri, jono, säikeiturvallinen jono).
  • Aggregator (usein: pääsäie tai käyttöliittymässä erillinen ajastin) laskee edistymätiedoista käyttöliittymän tilan (paikka, teksti, ETA) ja päivittää kontrollit. Näin vältetään 1:1-päivitykset per iteraatio.
  • Päättäminen (myös pääsäikeessä): käyttöliittymän uudelleenaktivointi, tuloksen näyttäminen, virheiden kokoaminen, resurssien vapauttaminen.

Miksi tämä erottelu toimii niin hyvin: työkuorma voi olla korkean frekvenssin (tuhansia elementtejä), mutta UI tarvitsee vain muutamia päivityksiä sekunnissa. Käytännössä 5–10 päivitystä/s riittää, erittäin nopeissa töissä jopa 2–4. Kaikki sen yli on usein pelkkää visuaalista kohinaa ja kuluttaa CPU-aikaa Message Pumpissa.

Säikeiturvallinen laskenta: atominen operaatio lukon sijaan

Yksinkertaiseen ProgressBariin riittää usein atominen laskuri. „Atominen“ tarkoittaa, että inkrementointi ja luku tapahtuvat ilman kilpaolosuhteita, tyypillisesti TInterlocked-kutsun avulla. Näin vältetään lukot (Critical Sections) silmukan kuumassa polussa.

Hyväksi todettu minimikonsepti:

  • Kokonaismäärä tunnetaan etukäteen (esim. tietueiden, tiedostojen, ID:iden lukumäärä).
  • Jokainen iteraatio kasvattaa atomisesti DoneCounter.
  • UI-ajastin lukee laskuria periodisesti ja asettaa ProgressBar.Position.

Etuna: ei TThread.Queue-kutsua per elementti, ei UI:n ylikuormitusta. Haittana: et saa yksityiskohtaisia ilmoituksia per elementti (esim. tiedostonimeä). Tätä voi täydentää toisella, rajoitetulla tila-ilmoituksella (katso seuraava osio).

Tila-ilmoitukset ilman spämmiä: „viimeinen tila voittaa“

Jos haluat lisäksi näyttää lyhyttä tekstiä (nykyinen elementti, vaihe, virheilmoitus), tarvitaan malli, joka ei tulvi käyttöliittymää jokaisesta worker-askeleesta. Käytännössä „viimeinen tila voittaa“ toimii erittäin hyvin:

  • Worker kirjoittaa tila­tiedon säikeiturvalliseen rakenteeseen (esim. atomisesti vaihdettavaan merkkijonoon tai pieneen lukkoon suojattuna).
  • UI-ajastin siirtää periodisesti viimeksi nähtyjen tilan labeliin.

Tällä tavalla UI pysyy reagoivana, ja näet silti, että „jotain tapahtuu“. Tärkeämpää kuin merkkijono itse on sen elinkaari: älä kuljeta viitteitä worker-säikeiden lyhytikäisiin objekteihin UI-säikeeseen. Jos siirrät objekteja, määritä niiden Ownership selkeästi.

TParallel.For -säikeiturvallinen Progress-UI TThread.Queue’n kanssa: robusti malli

On tilanteita, joissa UI-ajastin ei yksin riitä: esimerkiksi kun haluat varmistaa, että lopussa lähetetään täsmälleen yksi „Fertig“-päivitys, tai kun UI-päivitys on monimutkaisempi toimenpide (esim. merkintä loki-ikkunaan, mutta rajoitettuna). Silloin TThread.Queue on sopiva – mutta ei per iteraatio, vaan kohdennetusti.

Käytännöllinen lähestymistapa on jonotus vain vähäfrekvenssisille tapahtumille:

  • Start-Event (UI vorbereiten, Buttons sperren)
  • Periodische Progress-Events (max. alle X Millisekunden)
  • Fehler-Events (optional gesammelt)
  • Done-Event (UI zurücksetzen, Ergebnis anzeigen)

Periodisuuden saavutatte ei UI-säikeessä vaan jo worker-kontekstissa: annat workerin queueata UI-päivityksen vain, jos viimeisestä UI-päivityksestä on kulunut riittävästi aikaa. Siihen sopii monotoni aikalähde (esim. TickCount) sekä atominen „last update“ -arvo.

Tärkeää: UI-päivityksen itsessään on oltava „nopea“. Kalliit laskelmat, tiedosto-I/O tai tietokantahaut eivät kuulu gequeuettuun UI-callbackiin. Callbackin pitäisi vain lukea tiloja ja asettaa kontrollit.

Cancel-Handling: Abbrechen ohne Hänger

Todellisissa sovelluksissa peruutus ei ole valinnainen. Keskeistä on: Cancel ei ole „Kill“, vaan een kooperatiivinen päättäminen. Workerien täytyy säännöllisesti tarkistaa, onko peruutussignaali asetettu, ja poistua sen jälkeen siististi. Delphi:ssa on tähän useita tapoja (riippuen PPL-rakenteesta): oma Volatile-lippu, atominen Boolean, tai Cancellation-konsepti tehtävien kautta (riippuen Delphi-versiosta ja -rakenteesta).

Käytön kannalta tärkeitä ovat kaksi sääntöä:

  • Peruutuksen pitää näkyä nopeasti: Tarkista keskeytysflagi tarkoituksenmukaisissa kohdissa, ei vain iteraation lopussa, jos iteraatio voi kestää sekunteja.
  • Peruutuksen on siivottava: Avoimia handleja, väliaikaisia tiedostoja, transaktioita tai lukkoja ei saa jättää jäljelle. Eli: jokaisessa worker-iteraatiossa ovat try/finally-lohkot pakollisia, kun resursseja on mukana.

Käyttöliittymässä peruutuksen pitäisi vain asettaa signaali ja siirtää UI „Pysäytetään…“-tilaan. Varsinainen lopetus ja käyttöliittymän uudelleenaktivointi tapahtuvat sitten Done-tapahtumassa, eivät heti klikkauksessa.

Deadlockien välttäminen: käytännön yleisimmät sudenkuopat

Kaavakuva säikeiden välisestä syklisestä odotuksesta deadlockin visualisointina
Deadlockit syntyvät usein syklisestä odottamisesta: UI-lanka estyy, workerit odottavat UI-kutsua.

Sudenkuoppa 1: WaitFor/Task.Wait pääsäikeessä

Kun pääsäie on estynyt, se ei voi suorittaa Queue-takaisinkutsuja eikä käsitellä viestejä. Tämä vaikuttaa deadlockilta, vaikka workerit jatkaisivat oikein. Ratkaisu: ei estäviä Wait-kutsuja UI-langassa. Sen sijaan lopetustoiminto TThread.Queue:n kautta tai tapahtumahallinta (esim. ajastin tarkistaa „valmis“).

Sudenkuoppa 2: Synchronize lukon sisällä

Klassikko: worker pitää kriittistä osiota, kutsuu sitten Synchronize, ja UI-kutsussa (suoraan tai epäsuorasti) tarvitaan taas sama kriittinen osio. Tulos: odotussilmukka. Sääntö on yksinkertainen: ei UI-siirtoa (Synchronize/Queue) pidetystä lukosta käsin. Jos lukko on tarpeen, hae kaikki tiedot ensin paikallisiin muuttujiin, vapauta lukko, ja vasta sitten jonota.

Sudenkuoppa 3: UI-kutsun aiheuttama uudelleensisäänpääsy (Reentrancy)

Joskus UI-päivitys ei ole „harmiton“: ominaisuuksien asettaminen voi laukaista tapahtumia (OnChange, OnResize), jotka puolestaan käynnistävät logiikkaa, joka pääsee worker-tiloihin. Tämä ei ole deadlock kapeassa merkityksessä, mutta johtaa vaikeasti selitettäviin jumiin ja race-tilanteisiin. Ratkaisu: sijoita UI-päivitykset „hiljaisiin“ polkuihin (poista tapahtumat väliaikaisesti käytöstä) tai käytä reentrancy-suojia (esim. atominen suoja päivitysvaiheelle).

Sudenkuoppa 4: Liian monta jonotettua päivitystä

Ilman Waiteja UI voi silti „seistä“, jos tuot kymmeniätuhansia jonotettuja takaisinsoittoja. Oireita: ProgressBar jatkaa pitkään, ikkuna reagoi hitaasti, pääsäikeen CPU-käyttö korkea. Ratkaisu: hidasta (aikaviipale), aggregoi (laskuri) tai käytä todellista tuottaja/kuluttaja-rakennetta, jossa vain yksi UI-päivitys voi olla „odottavana“ (yhdistäminen/coalescing).

Kun tilanteet monimutkaistuvat: tulosten keräys, virheiden yhdistäminen, järjestyksen takaaminen

TParallel.For on ihanteellinen, kun iteraatiot ovat riippumattomia. Liiketoimintaohjelmistoissa iteraatiot ovat kuitenkin usein vain „suurin piirtein“ riippumattomia: luetaan tiedostoja, kutsutaan REST-APIja, kirjoitetaan tietokantarivejä. Silloin sinun on suunniteltava kolme lisäpistettä huolellisesti:

  • Säikeiturvallinen tulosten keruu: Joko kutakin säiettä kohden paikallinen puskuri (yhdistetään lopuksi) tai säikeiturvallinen jono/kokoelma. Vältä lukkoja kriittisellä suorituspolulla.
  • Virheenkäsittely: Poikkeukset työsäikeistä on kerättävä. Käytännössä toimii: muista ensimmäinen poikkeus ja laukaise keskeytys, tai kerää kaikki poikkeukset ja näytä ne lopussa yhteenvedettynä.
  • Järjestys: Jos ulostulo tarvitsee vakaan järjestyksen (esim. lokit indeksin mukaan), on rinnakkainen käsittely ja jälkikäteen lajittelu usein helpompaa kuin „säieturvallinen järjestetty lisäys“.

Käyttöliittymän osalta tämä tarkoittaa: älä näytä jokaista virheilmoitusta heti. Se johtaa modaalisten dialogien tulvaan. Kerää virheet (esim. merkkijonolista) ja näytä lopussa yhteenveto tai exportoitava loki.

Virheenkorjaus: Kuinka tehdä deadlock todella näkyväksi

Deadlockit rinnakkaiskoodissa ovat turhauttavia, koska ne voivat debugg­erissa näyttää erilaisilta kuin release‑ajossa. Silti on muutama käytännöllinen keino:

Käytä Threads‑ikkunaa ja kutsupinoja

Kun UI jämähtää, tarkastele kaikkia säikeitä: missä päälanka on? Odottaako se? Onko se viestisilmukassa? Missä työsäikeet ovat? Jos työsäikeet jumittuvat Synchronize-kutsuun, syy on lähes aina „päälanka estynyt“ tai „päälanka tarvitsee lukon“.

Merkitse jono‑ ja Synchronize‑kohdat

Aseta kohdennettua lokitusta siirtokohtiin (ennen jonoa, jonon callbackissa, iteraation lopussa). Ajojärjestelmässä tämä on usein arvokkaampaa kuin breakpointit, koska ajoitus on ratkaisevaa. Varmista, että lokitus itsessään on säieturvallista eikä estävää (esim. ei suoraa UI‑lokitulostusta työsäikeistä).

Mittaa viimepäivityksen aika

Jos UI „jäätyy“, syynä voi yksinkertaisesti olla se, että sen on käsiteltävä liikaa päivityksiä. Mittaa siksi päälangassa, kuinka monta UI‑päivitystä suoritat sekunnissa ja kuinka kauan kukin kestää. Kun UI‑callbackit vievät yli muutaman millisekunnin, tarvitaan rajoittamista tai yksinkertaistamista.

Milloin TParallel.For progress‑UI:n kanssa todella kannattaa?

Rinnakkaistaminen ei ole itseisarvo. Se kannattaa erityisesti, kun:

  • iteroinnit ovat riittävän suuria (millisekunneista sekunteihin), jotta threadpoolin overhead häviää,
  • työ on CPU‑intensiivistä (Parsing, Kompression, Hashing) tai sillä on hyvin rinnakkaistettavaa I/O:ta (useita tiedostoja, useita HTTP‑pyyntöjä rajoituksin),
  • sinulla on selkeä peruutus‑ ja virhestrategia,
  • UI‑vaatimukset selviävät aggregoidulla edistymällä.

Se kannattaa vähemmän, jos jokainen iteraatio on erittäin lyhyt (mikro‑operaatiot) tai jos kaikki iteraatiot ajavat samaan pullonkaulaan (sarjainen DB‑transaktio, globaali lukko, yksittäinen tiedosto). Tällöin nopeampi keino on usein: parantaa algoritmia, tehdä ryhmittelyä (batchaus), vähentää datan hakua tai irrottaa pullonkaula eksplisiittisesti.

Käytännön tarkistuslista: Näin UI pysyy vakaana

  • Päälanka ei esty: ei odotuksia (Wait), ei pitkiä silmukoita ilman viestipumppua.
  • Työsäikeet eivät koskaan koske kontrollikomponentteihin: ei VCL/FMX‑kutsuja UI‑säikeen ulkopuolella.
  • UI‑päivitykset ovat rajoitettuja: laskuri/timer tai yhdistetyt jonopäivitykset per iteraation sijaan.
  • Ei Synchronize‑kutsuja lukkojen sisältä.
  • Peruuttaminen on kooperatiivista, tarkistetaan usein ja siivoaa asianmukaisesti.
  • Poikkeukset kerätään ja käsitellään lopuksi järjestelmällisesti.

Yhteenveto: Irrota prosessit jonolla, vakaus aggregaatiolla

Robustin edistymisen käyttöliittymän TParallel.For-käyttöönotto ei synny „johonkin Synchronizeä sisään“ -ratkaisuilla, vaan selkeällä arkkitehtuuriperiaatteella: workerit toimivat itsenäisesti, pääsäie pysyy vapaana ja käsittelee vain muutamia, nopeita käyttöliittymäpäivityksiä. TThread.Queue on oikea työkalu, kun sitä käytetään tarkoituksellisesti ja rajoitetusti. „Tukkeutuva“ tai epävakaa käyttäytyminen johtuu lähes aina kahdesta syystä: pääsäie odottaa jossain blokkaavasti – tai se hukkuu liian moneen jonotettuun päivitykseen.

Kun malli on kerran siististi asetettu (Counter/Coalescing, Cancel-Flag, Abschluss-Callback), se kannattaa ottaa käyttöön monissa kohdissa kasvaneessa Delphi-sovelluksessa: Import/Export, tietotarkistukset, tiedosto- ja API-työt – kaikki muuttuu responsiivisemmaksi ilman, että jokaista edistymispäivitystä kohden saa uusia deadlockeja.

Jos tarvitsette tukea parallel-koodin vakauttamisessa, käyttöliittymän jumitusten debuggaamisessa tai kasvaneiden Delphi-sovellusten siistissä modernisoinnissa: ota yhteyttä.

Tähän aiheeseen liittyvät myös Delphi Parallel Programming Library ja Tthread.queue Vs Synchronize. Artikkeli asettaa nämä näkökohdat ymmärrettävään kontekstiin ja osoittaa, mihin arjessa kannattaa kiinnittää huomiota.

Keskustele projektista tai modernisointihankkeesta Net-Base kanssa.

Seuraava vaihe

Kun aiheesta muodostuu todellinen projekti, arkkitehtuuri, nykytila ja operointi on tarkasteltava yhdessä varhaisessa vaiheessa.

Emme tue pelkästään yksittäiskysymyksissä, vaan myös silloin, kun lähdekoodipalasista, legacy-aiheista tai portaali-ideoista halutaan muodostaa luotettava yrityshanke.

  • Nykytila, tavoitetila ja tekniset riskit arvioidaan yhdessä.
  • REST, tietojen käyttö, portaalit ja käyttöönotto eivät siirry myöhempään vaiheeseen.
  • Näette ajoissa, mikä vaihtoehto on taloudellisesti ja operatiivisesti kannattava.

Jaa artikkeli

Jaa tämä viesti suoraan

LinkedIn, X, XING, Facebook, WhatsApp ja sähköposti ovat välittömästi saatavilla. Instagramia varten valmistelemme linkin ja lyhyen tekstin.

Sähköposti

Instagram avautuu uuteen välilehteen. Linkki ja lyhyt teksti kopioidaan ensin leikepöydälle.