Umíme to s Delphi, 56. díl – vlákna a paralelní programování: dokončení

Dnes dokončujeme téma vláken v Delphi. Povíme si, jak pracovat s vlákny bez použití třídy TThread, vysvětlíme si problematiku lokálních dat vláken a kromě jiného si také ukážeme, jak zajistit bezchybný běh vícevláknových programů. Dovedete si představit, jak ladit vlákna, když běží nezávisle na sobě?

Vlákna v Delphi bez třídy TThread

Dosud jsme pracovali s vlákny výhradně pomocí třídy TThread. Práce je to velmi komfortní, nicméně nejde samozřejmě o jedinou možnost. Navíc si musíme uvědomit, že třída TThread používání vláken v maximální možné míře zjednodušuje – práce s vlákny pomocí této třídy se ani moc nepodobá práci s vlákny v prostředí Windows API. Tedy – ne že by to většinou bylo na škodu…:)

Pokud bychom chtěli rozebrat možnosti práce s vlákny od nejnižší úrovně, musíme začít u funkce Windows API CreateThread. Tato funkce slouží ve Win32 k vytvoření vlákna, nemusíte ji volat takto přímo, protože přesně totéž za vás udělá funkce BeginThread (viz další odstavec) zapouzdřující WinAPI funkci CreateThread.

Další možností, která se již často používá, je volání funkce BeginThread. Tato funkce je již definována přímo v Delphi (konkrétně jednotkou System), a její volání je zcela bezpečné. Práce s touto funkcí poměrně věrně kopíruje práci s vlákny ve Win32 (zjednodušeně řečeno – vytvoří se vlákno, předá se mu ukazatel na funkci, která bude provádět „jeho“ činnost (z tohoto konceptu vychází metoda Execute třídy TThread), provede se předání parametrů a vlákno se rozběhne). Ukončení běhu vlákna se provede zavoláním funkce EndThread a odstranění vlákna z paměti pak funkcí CloseHandle.

Když jsme pracovali s třídou TThread a potřebovali jsme mít pro každé vlákno nějaká lokální data (lokální proměnné vlákna), nebylo nic jednoduššího, než je zahrnout do deklarace třídy příslušného vlákna. Vznikly tak atributy vlákna, přičemž každé vytvořené vlákno mělo samozřejmě „své vlastní“.

Zkuste se ale zamyslet nad tím, jak zajistit vláknům podobnou možnost při práci bez třídy TThread. Když vytvoříme globální proměnnou, bude fyzicky existovat jen jedna, shodná pro všechna vlákna. Jakmile jedno vlákno změní hodnotu této globální proměnné, budou ostatní vlákna „vidět“ a číst tuto novou hodnotu. Jak zajistit lokální proměnnou pro vlákno vytvořené funkcí BeginThread?

Lokální data vláken

Při práci s vlákny pomocí rutin BeginThread, resp. EndThread občas skutečně potřebujeme globální proměnnou, která by ovšem pro každé vlákno mohla nabývat jiné hodnoty. Delphi samozřejmě nabízí řešení: klíčové slovo threadvar.

Když definujete globální proměnnou pomocí klíčového slova var, tedy „klasickým způsobem“, bude globální (leč společná) pro všechna vlákna daného procesu. Nadefinujete-li ji však pomocí klíčového slova threadvar, bude mít každé nově vytvořené vlákno svou vlastní kopii této proměnné. Všechna vlákna tedy tuto proměnnou „uvidí“ (je globální), nicméně každé vlákno si v ní bude moci uchovávat svá soukromá data.

Proměnné threadvar, kromě toho, že jejich použití je pomalé, mají řadu omezení (nemohou být např. typu ukazatel, apod.), nicméně obecně představují velmi silnou zbraň při práci s vlákny pomocí zmíněných funkcí. Z prostorových důvodů si je bohužel už nemůžeme prakticky předvést v aplikaci.

Funguje náš vícevláknový program správně?

Předposledním tématem, kterým se budeme zabývat, je určení správnosti vícevláknového programu. Ukážeme si, jak poznat, že aplikace funguje správně. V dalším odstavci si také prozradíme, co dělat zjistíme-li, že správně nefunguje. Začneme opět (snad snesitelnou) trochou teorie.

V paralelním programu (v programu používajícím vlákna) probíhá výpočet tzv. nedeterministicky, tj. předem neznámým způsobem. Děje se tak na rozdíl od „obyčejného“, „sériového“, tedy neparalelního programu, ve kterém dokážeme v každém okamžiku vždy přesně určit, kde bude následovat provádění programu. V paralelním programu je posloupnost stavů, kterými prochází vlákno i celá aplikace, pokaždé jiná. V různých bězích téhož programu tedy mohou pro stejná vstupní data střídavě nastávat následující situace:

  • program neskončí (zhavaruje),
  • program sice skončí, ale dá špatný výsledek,
  • (málokdy) program skončí se správným výsledkem.
Jak poznáme, že program skončil se správnými výsledky? Program (obecně, neplatí to jen pro paralelní programy) je správný, když splňuje všechny následující podmínky:
  • v každém běhu skončí,
  • pro stejná vstupní data (pro všechny možnosti vstupních dat) dá pokaždé tentýž správný výsledek.
Pokud jsou tyto podmínky splněny, program je kompletně správný. Nicméně dosažení tohoto stavu je obvykle poněkud netriviální, proto projevujeme radostné emoce i v případě, že se nám podaří zajistit jednu z dvou „podmnožin“ správnosti:
  • flow-correct (správnost z hlediska řízení) – program vždycky skončí a dá správný výsledek,
  • logicall-correct (logická správnost) – je-li výsledek správný pro veškeré myslitelné kombinace vstupních dat.
Důležité je nejdříve zajistit správné řízení (tedy flow-correct). Povíme si několik obecných informací o tom, jak flow-correct dosáhnout:
  • zajistit bezpečnost manipulace se sdílenými daty – žádná proměnná se nemění „nepředvídatelně“, jsou správně ošetřené kritické sekce,
  • zajistit živost – dvě či více vláken se nesmí vzájemně blokovat (jedno čeká na výsledek druhého, to zase na výsledek prvního), je nutné správně ošetřit synchronizace,
  • zajistit poctivost – všechna vlákna musí mít možnost využívat zdroje výpočtu (alespoň občas musí dostat kousek času, aby mohla počítat). Tento bod obvykle zařizuje operační systém, ale nevhodným chováním mu v tom můžeme úspěšně zamezit, např. nesprávným stanovením priorit, apod.

Co dělat, když program nefunguje?

V minulém odstavci jsme se naučili rozpoznávat funkčnost vytvořeného programu. Připomeňme, že zdaleka nejde jen o vícevláknové programy – uvedené pravdy jsou použitelné pro jakýkoliv program.

Když zjistíme, že program nefunguje podle našich představ, nastupuje fáze ladění a známého „odvšivování“ (debug). Ve vícevláknových programech ovšem nemůžeme využívat konvenční prostředky ladění. Proč?

Jedním z nejpoužívanějších prostředků ladění (alespoň pro mě) je krokování programu. Nejprve na nějakou řádku zdrojového kódu umístím bod přerušení (Breakpoint), Run – Add Breakpoint – Source Breakpoint), a když se vykonávání programu na tomto místě zastaví, krokuji pomocí kláves F7 nebo F8 (Trace Into, resp. Step Over). Přitom sleduji obsahy některých proměnných, apod. Tento postup ovšem funguje jen u neparalelního programu (přičemž ani tam to neplatí vždycky – v událostmi řízených programech už nám přicházející události dělají pěkný „zmatek“).

V paralelním programu je tento postup v zásadě k ničemu. Představte si, že potřebujete odladit činnost, kterou provádí několik vláken. Vidíme, že nějaká globální proměnná má na konci špatnou hodnotu. Je hezké, že si umístíme někam do kódu bod přerušení, na kterém se vykonávání programu zastaví, ovšem běží-li podle příslušné funkce více vláken, stejně nevíme, které z nich vlastně zastavení způsobilo.

Obecně řečeno: ladíme-li vícevláknovou aplikaci, může být velmi problematické sledovat stav všech vláken prováděných paralelně. Ještě obtížnější je určit, které z vláken právě běží v okamžiku, kdy zabere bod přerušení. Vhodným řešením je použití speciálního nástroje, který Delphi nabízí – dialogu ThreadStatus.

Abyste si tento dialog zobrazili, vyberte z hlavní nabídky View – Debug Windows – Threads, případně stiskněte klávesovou zkratku Ctrl – Alt – T.

Jakmile poté zabere nějaká „ladicí“ událost (např. bod přerušení, výjimka, pozastavení programu, doběhnutí ke kurzoru, apod.), dialog ukazuje stav všech právě běžících vláken. Pokud klepnete pravým tlačítkem na některé vlákno, rozvine se seznam příkazů, pomocí nichž můžete prohlížet přímo stav procesoru (View Source, Go to Source, doporučuji ovšem jen skutečným fajnšmekrům:-)). Kromě toho ale můžete provést ještě jednu užitečnou věc: nastavit některé vlákno aktivním. Nastavíte-li některé vlákno aktivním, následující krok (kroky) běhu programu po opětovném obnovení běhu (např. F9) bude prováděný právě tímto vláknem.

V dialogu Thread Status jsou všechna vlákna zobrazována i s jejich identifikačním číslem. Pokud používáte vlákna na základě třídy TThread, je jejich identifikační číslo obsahem vlastnosti ThreadID. Nepoužíváte-li objekty vláken (třídu TThread), je ID každého vlákna vraceno při jeho vytvoření funkcí BeginThread. Kromě vláken jsou v tomto dialogu zobrazovány i případné procesy, které jsme vytvořili.

Pojďme si dialog Thread Status ukázat v konkrétním případě. Vrátíme se k naší vícevláknové MDI aplikaci. Umístíme třeba do zdrojového kódu v modulu Childwin.PAS bod přerušení, a to někam dovnitř cyklu v metodě TMDIVlakno.Execute. Řekneme také, aby tento bod přerušení zabral až po 1000 průbězích – tedy až po vykreslení 1000 čar.

Umístěte kurzor na příslušnou řádku zdrojového textu, vyberte Run – Add Breakpoint – Source Breakpoint (v Delphi 5). V otevřeném dialogu zadejte do pole Pass Count hodnotu 1000:

Klepněte pro větší obrázek

Než aplikaci spustíme, otevřeme si ještě dialog Thread Status. Nyní aplikaci spusťte a rychle vytvořte asi pět nebo šest nových oken. Pak už jen počkejte, až se vykonávání zastaví a prohlédněte si obsah dialogu Thread Status:

Klepněte pro větší obrázek

V Delphi existuje ještě řada dalších prostředků pro ladění procesů a vláken (např. Run – Attach to Process, nicméně zabývat se jimi již nebudeme.

Používání vláken v distribuovaných aplikacích

V našem seriálu jsme se dosud distribuovanými aplikacemi nezabývali (ani jakýmikoliv jinými aplikacemi neběžícími celé na lokálním počítači). Aby ale tento popis vláken byl ucelený, uvedeme si velmi stručně několik zásad, kterými se řídí práce s vlákny v distribuovaných aplikacích.

Distribuované aplikace představují pro vícevláknové programování další výzvu. Při úvahách, jak vlákna v takových aplikacích koordinovat, musíme mít na zřeteli také další procesy, které mohou ovlivňovat vlákna v naší aplikaci.

Obvyklé schéma distribuované aplikace si lze představit jako odpovědnost serveru za splnění požadavků klientů. Pokud každý klient používá své vlastní vlákno, je nutné zajistit, aby se tato vlákna klientů neovlivňovala navzájem. Na druhou stranu se ale může stát, že pokud jeden klient změní nějaký globální objekt, změny by se měly promítnout do ostatních vláken – je zřejmé, že v takovém případě nepůjde použít lokálních dat vláken (threadvar).

Práce s vlákny v distribuovaných aplikacích je ještě mnohem složitější, než v případě lokálních aplikací.

Na úplný závěr

Možná vás potěším, možná méně – ale právě jsme již skutečně, definitivně a neodvratně dokončili popis problematiky vláken. Věřím, že v dnešním dílu a v předchozích pěti dílech jste se dozvěděli celou řadu informací, které vám umožní vytvářet plnohodnotné aplikace používající vláken systému Windows. Pokud máte jiný názor, vyjádřete jej prosím v diskusi, případně mě kontaktujte na můj email.

Vlákna jsou velmi progresivním a mocným konceptem. Jejich správným (a vhodným) používáním je možné vytvořit skutečně velmi výkonné aplikace. Na druhou stranu si musíme uvědomit, že vlákna nepatří k úplně triviálním záležitostem – především z toho důvodu (vágně řečeno), že u vláken si nemůžeme být jistí vůbec ničím. Při programování vláken musíme zapomenout na řadu návyků „klasického programování“, které nám umožňovaly spolehnout se na to či ono. Když to však zvládneme, výsledky stojí za to…:)

Diskuze (5) Další článek: Šum v digitální fotografii

Témata článku: Software, Programování, Flow, Neznámý způsob, Nejnižší třída, Předposlední místo, Klíčová funkce, Paralelní, Status, Opětovné obnovení, TomTom Go, Následující řádek, Dok, Dialog, Program, Zavolání, Dokončení, Apod, Díl, Vlákno, Správná funkce, DEL, Pass, Špatná podmínka


Určitě si přečtěte

Jak se šíří Covid v Česku: Čerstvá data, mapy okresů a obcí. Každý den aktualizované grafy

Jak se šíří Covid v Česku: Čerstvá data, mapy okresů a obcí. Každý den aktualizované grafy

** Vývoj COVID-19 v Česku: nakažení, úmrtí, testovaní, hospitalizovaní ** Mapa podle okresů, přehled podle věku, situace v Evropě i ve světě ** Každý den aktualizované grafy a mapy

Marek Lutonský | 164

Google mapy, Seznam mapy, Apple mapy... Velké srovnání šesti internetových map. Kdo to dělá nejlépe?

Google mapy, Seznam mapy, Apple mapy... Velké srovnání šesti internetových map. Kdo to dělá nejlépe?

** Která klasická webová mapa se vám líbí nejvíce? ** Srovnali jsme šest velkých služeb v několika situacích ** Hlasujte v anketě

Jakub Čížek | 81

Zahodil jsem Windows, přešel na Linux a nezbláznil se z toho

Zahodil jsem Windows, přešel na Linux a nezbláznil se z toho

** Měsíc jsem se nedotkl Windows a byl závislý jen na Linuxu ** Jaká byla pozitiva a negativa přechodu? ** Se kterými aplikacemi jsem (ne)zápasil a které bych doporučil?

Lukáš Václavík | 245

10 věcí, které nás štvou na Windows 10 a bohužel asi jen tak nepřestanou

10 věcí, které nás štvou na Windows 10 a bohužel asi jen tak nepřestanou

** Windows 10 je na trhu 5 let, ale pořád má velké rezervy ** Ani desátá velká aktualizace, která vyjde na podzim, je nevyřeší ** Štvou nás Windows Update, Store, Nastavení atd.

Lukáš Václavík | 147

Nvidia představila grafické karty GeForce RTX 3090, RTX 3080 a RTX 3070. Známe české ceny

Nvidia představila grafické karty GeForce RTX 3090, RTX 3080 a RTX 3070. Známe české ceny

** Nvidia uvedla nové desktopové grafické karty GeForce RTX 3000 ** Jedná se o modely GeForce RTX 3070, 3080 a 3090 ** K výrobě se používá 8nm technologii od Samsungu

Karel Javůrek | 67


Aktuální číslo časopisu Computer

Velký test fitness náramků

Levné záložní zdroje

Jak si zabezpečit domov

Nejlepší monitory na trhu