Procesy a vlákna. Multitasking a multithreading

Clay Breshears

Úvod

Metody multithreadingu používané odborníky Intel zahrnují čtyři hlavní fáze: analýzu, vývoj a implementaci, ladění a ladění výkonu. Právě tento přístup se používá k vytvoření vícevláknové aplikace ze sekvenčního programového kódu. Pracujte s softwarových nástrojů v průběhu realizace první, třetí a čtvrté etapy je pokryta poměrně široce, zatímco o realizaci druhého kroku zjevně není dostatek informací.

Bylo publikováno mnoho knih o paralelních algoritmech a paralelním počítání. Tyto publikace však pokrývají hlavně předávání zpráv, systémy distribuované paměti nebo teoretické modely paralelních výpočtů, které někdy nejsou použitelné na skutečné vícejádrové platformy. Pokud jste připraveni vážně se zapojit do vícevláknového programování, určitě budete potřebovat znalosti o vývoji algoritmů pro tyto modely. Aplikace těchto modelů je samozřejmě značně omezená, takže je možná bude muset mnoho vývojářů softwaru stále zavádět do praxe.

Bez nadsázky lze říci, že vývoj vícevláknových aplikací je především tvůrčí činností a až poté činností vědeckou. V tomto článku se naučíte osm jednoduchých pravidel, která vám pomohou rozšířit vaši praxi paralelního programování a zlepšit efektivitu vláken ve vašich aplikacích.

Pravidlo 1. Vyberte operace prováděné v kódu programu nezávisle na sobě

Paralelní zpracování je použitelné pouze pro ty operace sekvenčního kódu, které se provádějí nezávisle na sobě. Dobrým příkladem toho, jak nezávislé jednání vede ke skutečnému jedinému výsledku, je stavba domu. Zahrnuje pracovníky mnoha specializací: tesaře, elektrikáře, štukatéry, klempíře, pokrývače, malíře, zedníky, krajináře a tak dále. Někteří z nich samozřejmě nemohou začít pracovat, dokud jiní svou práci nedokončí (např. pokrývači se pustí do práce až po postavení stěn a malíři nezačnou tyto stěny natírat, pokud nejsou omítnuté). Obecně ale můžeme říci, že všichni lidé, kteří se na stavbě podílejí, jednají nezávisle na sobě.

Vezměme si další příklad – pracovní cyklus půjčovny DVD, ve kterém přicházejí objednávky na určité filmy. Objednávky jsou distribuovány mezi zaměstnance výdejního místa, kteří tyto filmy hledají ve skladu. Samozřejmě, že pokud si některý ze zaměstnanců vezme ze skladu disk, na kterém je natočen film s Audrey Hepburnovou, nijak to neovlivní dalšího zaměstnance, který hledá další akční film s Arnoldem Schwarzeneggerem, a tím spíše to neovlivní jejich kolega, který hledá disky s novou sezónou Přátel. V našem příkladu předpokládáme, že všechny problémy s vyprodáním zásob byly vyřešeny dříve, než objednávky dorazí do půjčovny, a balení a expedice žádné objednávky neovlivní zpracování ostatních.

Ve své práci se pravděpodobně setkáte s výpočty, které lze zpracovávat pouze v určité posloupnosti, nikoli paralelně, protože různé iterace nebo kroky smyčky na sobě závisí a musí být prováděny v přísném pořadí. Vezměme si živý příklad z volné přírody. Představte si březí srnu. Vzhledem k tomu, že březost trvá v průměru osm měsíců, ať už se říká cokoliv, kolouch se neobjeví za měsíc, i když zabřezne osm jelenů současně. Osm jelenů současně by však svou práci odvedlo dokonale, pokud by byli všichni zapřaženi do Santových saní.

Pravidlo 2. Použijte rovnoběžnost s nízká úroveň detailování

Existují dva přístupy k paralelnímu oddělení sekvenčního programového kódu: „zdola nahoru“ a „shora dolů“. Za prvé, ve fázi analýzy kódu jsou identifikovány segmenty kódu (takzvané "horké" body), které zabírají významnou část doby provádění programu. Rozdělení těchto segmentů kódu paralelně (pokud je to možné) poskytne maximální zisk výkonu.

Přístup „zdola nahoru“ implementuje vícevláknové zpracování „horkých“ bodů kódu. Pokud paralelní rozdělení nalezených bodů není možné, měl by být prozkoumán zásobník volání aplikace, aby se určily další dostupné segmenty pro paralelní rozdělení a dostatečné provedení. na dlouhou dobu. Řekněme, že pracujete na aplikaci pro kompresi. grafické obrázky. Kompresi lze implementovat pomocí několika nezávislých paralelních vláken, která zpracovávají jednotlivé segmenty obrazu. I když se vám však podařilo implementovat multithreading „horkých“ míst, nezanedbávejte analýzu zásobníku volání, v důsledku čehož můžete najít segmenty, které jsou k dispozici pro paralelní rozdělení, umístěné více než vysoká úroveň programový kód. Tímto způsobem můžete zvýšit granularitu paralelního zpracování.

Přístup shora dolů analyzuje fungování programového kódu a vyčleňuje jeho jednotlivé segmenty, jejichž provedení vede k dokončení celého úkolu. Pokud neexistuje žádná zjevná nezávislost hlavních segmentů kódu, analyzujte jejich součásti a najděte nezávislé výpočty. Analýzou kódu můžete identifikovat moduly kódu, jejichž spuštění procesoru zabírá nejvíce času. Zvažte implementaci vláken v aplikaci určené pro kódování videa. Paralelní zpracování lze implementovat na nejnižší úrovni – pro nezávislé pixely jednoho snímku, nebo na vyšší úrovni – pro skupiny snímků, které lze zpracovávat nezávisle na ostatních skupinách. Pokud se vytváří aplikace pro zpracování více video souborů současně, paralelní dělení na této úrovni může být ještě jednodušší a úroveň detailů bude na nejnižší úrovni.

Granularita paralelních výpočtů se týká množství výpočtů, které je nutné provést před synchronizací mezi vlákny. Jinými slovy, čím méně často dochází k synchronizaci, tím nižší je granularita. Výpočty s jemnou strukturou vláken mohou způsobit, že režie systému spojená s organizováním vláken překročí množství užitečných výpočtů prováděných těmito vlákny. Zvýšení počtu vláken se stejným množstvím výpočtů komplikuje proces zpracování. Vícevláknové zpracování s nízkou granularitou způsobuje méně zpoždění systému a má větší potenciál pro škálování, kterého lze dosáhnout organizací dalších toků. Pro implementaci jemnozrnného paralelního zpracování se doporučuje použít přístup shora dolů a organizovat vlákna na vysoké úrovni zásobníku volání.

Pravidlo 3: Zahrňte do svého kódu škálovatelnost, aby se jeho výkon zvyšoval s počtem jader.

Není to tak dávno, co se na trhu kromě dvoujádrových procesorů objevily i čtyřjádrové. Intel navíc již oznámil vytvoření procesoru s 80 jádry, který je schopen provádět bilion operací s pohyblivou řádovou čárkou za sekundu. Vzhledem k tomu, že počet jader v procesorech se bude časem pouze zvyšovat, musí mít váš kód odpovídající potenciál pro škálovatelnost. Škálovatelnost je parametr, podle kterého lze posuzovat schopnost aplikace adekvátně reagovat na změny, jako je zvýšení systémových zdrojů (počet jader, velikost paměti, frekvence sběrnice atd.) nebo zvýšení objemu dat. Vzhledem k tomu, že počet jader v procesorech budoucnosti poroste, vytvořte škálovatelný kód, jehož výkon se zvýší díky nárůstu systémových zdrojů.

Abychom parafrázovali jeden ze zákonů C. Northecote Parkinson, můžeme říci, že „zpracování dat zabírá veškeré dostupné systémové prostředky". To znamená, že s přibývajícími výpočetními zdroji (například počtem jader) budou ke zpracování dat pravděpodobně využity všechny. Vraťme se k výše popsané aplikaci pro kompresi videa. Vzhled dalších jader v procesoru pravděpodobně neovlivní velikost zpracovávaných snímků - místo toho se zvýší počet vláken zpracovávajících snímek, což povede ke snížení počtu pixelů na stream. V důsledku toho se v důsledku organizace dalších vláken zvýší množství servisních dat a sníží se míra podrobnosti paralelismu. Dalším pravděpodobnějším scénářem by bylo zvýšení velikosti nebo počtu video souborů, které je třeba zakódovat. V tomto případě organizace dalších streamů, které budou zpracovávat objemnější (nebo další) videosoubory, umožní rozdělit celé množství práce přímo ve fázi, kde došlo ke zvýšení. Aplikace s takovými schopnostmi bude mít zase vysoký potenciál škálovatelnosti.

Návrh a implementace paralelního zpracování pomocí dekompozice dat poskytuje zvýšenou škálovatelnost ve srovnání s použitím funkční dekompozice. Počet nezávislých funkcí v programovém kódu je nejčastěji omezen a během provádění aplikace se nemění. Vzhledem k tomu, že každé nezávislé funkci je přiděleno samostatné vlákno (a tedy jádro procesoru), se zvýšením počtu jader dodatečně organizovaná vlákna nezpůsobí zvýšení výkonu. Modely paralelního dělení s dekompozicí dat tedy poskytnou zvýšený potenciál pro škálovatelnost aplikací díky skutečnosti, že s nárůstem počtu procesorových jader se zvýší množství zpracovávaných dat.

I když je programový kód vláknovým zpracováním nezávislých funkcí, je možné použít další vlákna, která se spouštějí při zvýšení vstupní zátěže. Vraťme se k výše popsanému příkladu stavby domu. Zvláštním účelem konstrukce je dokončit omezený počet nezávislých úkolů. Pokud však máte nařízeno postavit dvakrát tolik pater, pravděpodobně budete chtít najmout další pracovníky v některých specializacích (malíři, pokrývači, klempíři atd.). Proto musíte navrhovat aplikace, které se dokážou přizpůsobit rozkladu dat vyplývajícímu ze zvýšené zátěže. Pokud váš kód implementuje funkční rozklad, zvažte uspořádání dalších vláken s rostoucím počtem procesorových jader.

Pravidlo 4: Používejte knihovny bezpečné pro vlákna

Pokud možná budete potřebovat knihovnu pro zpracování dat v aktivních bodech kódu, určitě zvažte použití hotových funkcí místo vlastního kódu. Stručně řečeno, nesnažte se znovu objevit kolo vývojem segmentů kódu, jejichž funkce jsou již poskytovány v optimalizovaných procedurách z knihoven. Mnoho knihoven, včetně Intel® Math Kernel Library (Intel® MKL) a Intel® Integrated Performance Primitives (Intel® IPP), již obsahuje vícevláknové funkce optimalizované pro vícejádrové procesory.

Stojí za zmínku, že při použití procedur z vícevláknových knihoven se musíte ujistit, že volání jedné nebo druhé knihovny neovlivní normální provoz vláken. To znamená, že pokud jsou volání procedur prováděna ze dvou různých vláken, každé volání musí vrátit správné výsledky. Pokud procedury přistupují a aktualizují proměnné sdílené knihovny, může dojít k "datovému závodu", což nepříznivě ovlivní spolehlivost výsledků výpočtu. Pro správnou práci s vlákny je procedura knihovny přidána jako nová (to znamená, že neaktualizuje nic kromě lokálních proměnných) nebo synchronizována, aby byl chráněn přístup ke sdíleným zdrojům. Závěr: Před použitím jakékoli knihovny třetí strany v kódu programu si přečtěte přiloženou dokumentaci, abyste se ujistili, že funguje správně s vlákny.

Pravidlo 5: Použijte vhodný model závitování

Předpokládejme, že pro paralelní dělení všech vhodných segmentů kódu zjevně nestačí funkce ze složení vícevláknových knihoven a museli jste myslet na organizaci vláken. Nespěchejte s vytvářením vlastní (nepraktické) struktury vláken, pokud knihovna OpenMP již obsahuje všechny funkce, které potřebujete.

Nevýhodou explicitního multithreadingu je nemožnost přesné kontroly vlákna.

Pokud vše, co potřebujete, je paralelní sdílení smyček náročných na zdroje nebo zvláštní flexibilita, kterou vám explicitní vlákna poskytují, je na pozadí, pak v tento případ nemá smysl dělat práci navíc. Čím složitější je implementace multithreadingu, tím větší je pravděpodobnost chyb v kódu a tím obtížnější jeho následné zpřesňování.

Knihovna OpenMP je zaměřena na dekompozici dat a je vhodná zejména pro závitové smyčky, které pracují s velkým množstvím informací. Navzdory skutečnosti, že pro některé aplikace je použitelný pouze rozklad dat, je nutné vzít v úvahu dodatečné požadavky (například zaměstnavatele nebo zákazníka), podle kterých je použití OpenMP nepřijatelné a zbývá implementovat multithreading pomocí explicitních metod. V takovém případě lze OpenMP použít pro předvláknění k odhadu potenciálního zvýšení výkonu, škálovatelnosti a přibližného úsilí, které by bylo potřeba k následnému rozdělení kódu pomocí explicitního vícevláknového zpracování.

Pravidlo 6. Výsledek programového kódu by neměl záviset na pořadí provádění paralelních vláken

Pro sekvenční kód stačí jednoduše definovat výraz, který se provede po jakémkoli jiném výrazu. Ve vícevláknovém kódu není pořadí, ve kterém se vlákna provádějí, definováno a závisí na pokynech plánovače operačního systému. Přísně vzato je prakticky nemožné předvídat sekvenci vláken, která jsou spuštěna k provedení nějaké operace, nebo určit, které vlákno bude plánovačem spuštěno v následujícím okamžiku. Predikce se primárně používá ke snížení latence aplikací, zvláště když běží na platformě s procesorem, který má méně jader než organizovaná vlákna. Pokud je vlákno zablokováno, protože potřebuje přístup k oblasti, která není zapsána do mezipaměti, nebo protože potřebuje dokončit požadavek I/O, plánovač jej pozastaví a spustí vlákno připravené ke spuštění.

Přímým důsledkem nejistoty v plánování vláken jsou situace datového závodu. Předpoklad, že některé vlákno změní hodnotu sdílené proměnné dříve, než ji přečte jiné vlákno, může být chybný. S trochou štěstí zůstane pořadí spouštění vláken specifických pro platformu při všech spouštěních aplikací stejné. Nejmenší změny ve stavu systému (například umístění dat na pevném disku, rychlost paměti nebo dokonce odchylka od jmenovité frekvence střídavý proud elektrické sítě) může vyvolat jiné pořadí provádění vláken. U kódu, který správně funguje pouze s určitou posloupností vláken, jsou tedy pravděpodobné problémy spojené se situacemi závodu dat a uváznutím.

Z hlediska výkonu je vhodnější neomezovat pořadí provádění vláken. Přísná posloupnost provádění vláken je povolena pouze v případě nouze, určeného předem stanoveným kritériem. V případě takových okolností poběží vlákna v pořadí určeném poskytnutými synchronizačními mechanismy. Představme si například dva přátele, jak čtou noviny, které jsou rozložené na stole. Za prvé, umí číst jiná rychlost Za druhé, mohou číst různé články. A je jedno, kdo si šíření novin přečte jako první – v každém případě si bude muset na svého kamaráda počkat, než otočí stránku. Zároveň neexistují žádná omezení na čas a pořadí čtení článků – přátelé čtou libovolnou rychlostí a k synchronizaci mezi nimi dochází okamžitě po otočení stránky.

Pravidlo 7. Používejte místní úložiště streamů. Podle potřeby přiřaďte zámky jednotlivým datovým oblastem

Synchronizace nevyhnutelně zvyšuje zatížení systému, což v žádném případě neurychluje proces získávání výsledků paralelních výpočtů, ale zajišťuje jejich správnost. Ano, synchronizace je nutná, ale neměla by se zneužívat. Pro minimalizaci synchronizace se používá místní úložiště vláken nebo alokované oblasti paměti (například prvky pole označené identifikátory odpovídajících vláken).

Potřeba sdílet dočasné proměnné mezi různými vlákny je vzácná. Takové proměnné musí být deklarovány nebo přiděleny místně každému vláknu. Proměnné, jejichž hodnoty jsou mezivýsledky provádění vláken, musí být také deklarovány jako místní pro příslušná vlákna. Pro shrnutí těchto mezivýsledků v některé společné oblasti paměti bude vyžadována synchronizace. Aby se minimalizovalo možné zatížení systému, je vhodnější tuto společnou oblast aktualizovat co nejméně často. Explicitní metody vícevláknového zpracování poskytují rozhraní API místního úložiště pro vlákno, která zajišťují integritu místních dat od začátku provádění jednoho vícevláknového segmentu kódu do začátku dalšího segmentu (nebo během zpracování jednoho volání vícevláknové funkce až do dalšího provedení stejnou funkci).

Pokud místní ukládání vláken není možné, je přístup ke sdíleným prostředkům synchronizován pomocí různých objektů, jako jsou zámky. Důležité je správné přiřazení zámků konkrétním datovým blokům, což je nejjednodušší, pokud se počet zámků rovná počtu datových bloků. Jediný zamykací mechanismus, který synchronizuje přístup k více oblastem paměti, se použije pouze tehdy, když se všechny tyto oblasti nacházejí ve stejné kritické části kódu.

Co dělat, když je potřeba synchronizovat přístup k velkému množství dat, například k poli sestávajícím z 10 000 prvků? Uspořádání jediného zámku pro celé pole jistě znamená vytvoření úzkého hrdla v aplikaci. Opravdu je nutné organizovat blokování pro každý prvek zvlášť? Pak, i když k datům přistupuje 32 nebo 64 paralelních vláken, budete muset zabránit konfliktům přístupu k poměrně velké oblasti paměti a pravděpodobnost takových konfliktů je 1 %. Naštěstí existuje jakási zlatá střední cesta, tzv. „modulo locks“. Pokud je použito N modulo zámků, každý z nich bude synchronizovat přístup k N-té části celkové datové oblasti. Pokud jsou například uspořádány dva takové zámky, jeden z nich zabrání přístupu k sudým prvkům pole a druhý zabrání přístupu k lichým. V tomto případě vlákna, která přistupují k požadovanému prvku, určí jeho paritu a nastaví příslušný zámek. Počet modulo zámků se volí s ohledem na počet vláken a pravděpodobnost, že do stejné oblasti paměti bude současně přistupovat více vláken.

Všimněte si, že současné použití několika zamykacích mechanismů není povoleno pro synchronizaci přístupu do stejné oblasti paměti. Připomeňme si Segalův zákon: „Člověk, který má jedny hodinky, ví jistě, kolik je hodin. Člověk, který má pár hodin, si není jistý ničím. Předpokládejme, že přístup k proměnné je řízen dvěma různými zámky. V tomto případě může být první zámek převzat jedním segmentem kódu a druhý jiným segmentem. Potom se vlákna provádějící tyto segmenty ocitnou ve sporu pro sdílená data, ke kterým přistupují současně.

Pravidlo 8: Změňte programovací algoritmus, pokud je to nutné pro implementaci multithreadingu

Kritériem pro hodnocení výkonu aplikací, sériových i paralelních, je doba provádění. Asymptotické pořadí je vhodné jako odhad algoritmu. Podle tohoto teoretického ukazatele můžete téměř vždy hodnotit výkon aplikace. To znamená, že pokud jsou všechny ostatní věci stejné, aplikace s rychlostí růstu O(n log n) (rychlé řazení) bude fungovat rychleji než aplikace s rychlostí růstu O(n2) (selektivní řazení), i když výsledky těchto aplikací jsou stejný.

Čím lepší je asymptotické pořadí provádění, tím rychleji běží paralelní aplikace. Avšak ani ten nejproduktivnější sekvenční algoritmus nelze vždy rozdělit do paralelních vláken. Pokud je rozdělení aktivního bodu programu příliš obtížné a neexistuje způsob, jak implementovat multithreading na vyšší úrovni zásobníku volání tohoto aktivního bodu, měli byste nejprve zvážit použití jiného sekvenčního algoritmu, který je snazší rozdělit než ten původní. Samozřejmě existují i ​​jiné způsoby, jak připravit kód pro zpracování vláken.

Jako ilustraci posledního tvrzení uvažujme násobení dvou čtvercových matic. Strassenův algoritmus má jeden z nejlepších asymptotických prováděcích příkazů: O(n2.81), který je mnohem lepší než řád O(n3) konvenčního algoritmu trojité vnořené smyčky. Podle Strassenova algoritmu je každá matice rozdělena do čtyř podmatic, po kterých je provedeno sedm rekurzivních volání pro vynásobení n/2 × n/2 podmatic. Chcete-li paralelizovat rekurzivní volání, můžete vytvořit nové vlákno, které bude postupně provádět sedm nezávislých násobení podmatice, dokud nedosáhnou dané velikosti. V tomto případě se bude počet vláken zvyšovat exponenciálně a míra podrobnosti výpočtů prováděných každým nově vytvořeným vláknem se bude zvyšovat se zmenšováním velikosti podmatic. Zvažte další možnost - organizování skupiny sedmi vláken pracujících současně a provádění jednoho násobení podmatic. Po dokončení fondu vláken dojde k rekurzivnímu volání Strassenovy metody pro násobení podmatice (jako v sekvenční verzi programového kódu). Pokud má systém, na kterém běží takový program, více než osm procesorových jader, některá z nich budou nečinná.

Algoritmus násobení matic je mnohem jednodušší podřídit paralelnímu dělení pomocí trojité vnořené smyčky. V tomto případě se používá dekompozice dat, kdy se matice rozdělují na řádky, sloupce nebo podmatice a každý z proudů provádí určité výpočty. Implementace takového algoritmu se provádí pomocí pragmat OpenMP vložených na některé úrovni smyčky nebo explicitním uspořádáním vláken, která provádějí dělení matic. K implementaci tohoto jednoduššího sekvenčního algoritmu je potřeba mnohem méně úprav kódu ve srovnání s implementací vícevláknového Strassenova algoritmu.

Nyní tedy znáte osm jednoduchých pravidel pro efektivní převod sériového kódu na paralelní. Dodržováním těchto pravidel rychle vytvoříte vícevláknová řešení, která nabízejí zvýšenou spolehlivost, optimální výkon a méně úzkých míst.

Pro návrat na webovou stránku školení pro vícevláknové programování přejděte na

Jaké téma způsobuje začátečníkům nejvíce otázek a problémů? Když jsem se na to zeptal učitele a programátora Java Alexandra Pryakhina, okamžitě odpověděl: "Multithreading." Děkujeme mu za nápad a pomoc při přípravě tohoto článku!

Nahlédneme do vnitřního světa aplikace a jejích procesů, přijdeme na to, co je podstatou multithreadingu, kdy je užitečný a jak jej implementovat – na příkladu Javy. Pokud se učíte další OOP jazyk, nebojte se: základní principy jsou stejné.

O proudech a jejich původu

Abychom porozuměli multithreadingu, nejprve si ujasněme, co je to proces. Proces je část virtuální paměti a prostředků, které OS přiděluje pro provádění programu. Pokud otevřete několik instancí jedné aplikace, systém pro každou přidělí proces. V moderních prohlížečích může být za každou kartu zodpovědný samostatný proces.

Pravděpodobně jste narazili na Windows „Správce úloh“ (v Linuxu je to „Monitor systému“) a víte, že běžící procesy navíc zatěžují systém a ty „nejtěžší“ z nich často zamrzají, takže je nutné je násilně ukončit.

Uživatelé však milují multitasking: nekrmte chlebem – nechte je otevřít tucet oken a skákat tam a zpět. Nastává dilema: musíte zajistit souběžný chod aplikací a zároveň snížit zátěž systému, aby nedocházelo k jeho zpomalování. Řekněme, že hardware nedokáže držet krok s potřebami majitelů – problém je potřeba vyřešit na úrovni programu.

Chceme, aby procesor vykonával více příkazů a zpracovával více dat za jednotku času. To znamená, že se do každého časového řezu musíme vejít více, než je spuštěný kód. Představte si jednotku provádění kódu jako objekt – to je vlákno.

Ke složitému úkolu se snáze přistupuje, pokud je rozčleněn na jednodušší. Totéž platí při práci s pamětí: „těžký“ proces je rozdělen do vláken, která zabírají méně zdrojů a rychleji přinášejí kód do kalkulačky (jak přesně je uvedeno níže).

Každá aplikace má alespoň jeden proces a každý proces má alespoň jedno vlákno, které se nazývá hlavní vlákno a ze kterého se v případě potřeby spouštějí nové.

Rozdíl mezi vlákny a procesy

    Vlákna využívají paměť přidělenou procesu a procesy pro sebe vyžadují samostatné místo v paměti. Vlákna se proto vytvářejí a dokončují rychleji: systém pro ně nemusí pokaždé přidělovat nový adresní prostor a poté jej uvolňovat.

    Každou práci zpracovává s vlastními daty – něco si mohou vyměňovat pouze prostřednictvím mechanismu meziprocesové komunikace. Vlákna si navzájem přistupují ke svým datům a zdrojům přímo: to, co někdo změnil, je okamžitě k dispozici všem. Vlákno může ovládat své „bratry“ v procesu, zatímco proces ovládá pouze své „dcery“. Přepínání mezi streamy je proto rychlejší a komunikace mezi nimi je snazší.

Jaký z toho plyne závěr? Pokud potřebujete zpracovat velké množství dat co nejrychleji, rozdělte je na části, které lze zpracovat samostatnými vlákny, a poté výsledek shromážděte. To je lepší než vytvářet procesy náročné na zdroje.

Proč by ale aplikace tak populární jako Firefox šla cestou vytváření více procesů? Protože izolovaná práce karet je pro prohlížeč spolehlivá a flexibilní. Pokud je v jednom procesu něco v nepořádku, není nutné dokončit celý program – je možné zachránit alespoň část dat.

Co je multithreading

Zde se dostáváme k hlavnímu bodu. Multithreading je, když je proces aplikace rozdělen do vláken, která jsou zpracovávána paralelně – v jedné časové jednotce – procesorem.

Výpočetní zátěž je sdílena mezi dvěma nebo více jádry, aby se rozhraní a další programové komponenty vzájemně nezpomalovaly.

Vícevláknové aplikace lze provozovat i na jednojádrových procesorech, ale vlákna se pak spouštějí postupně: první fungovalo, jeho stav byl uložen - nechali pracovat druhé, uložili - vrátili se k prvnímu nebo spustili třetí atd.

Zaneprázdnění lidé si stěžují, že mají jen dvě ruce. Procesy a programy mohou mít tolik rukou, kolik je potřeba k co nejrychlejšímu dokončení úkolu.

Čekejte na signál: Synchronizace ve vícevláknových aplikacích

Představte si, že se několik vláken pokouší změnit stejnou datovou oblast současně. Čí změny budou nakonec přijaty a čí změny budou zrušeny? Aby se předešlo zmatkům při práci se sdílenými prostředky, vlákna potřebují koordinovat své akce. K tomu si vyměňují informace pomocí signálů. Každé vlákno říká ostatním, co právě dělá a jaké změny lze očekávat. Synchronizují se tedy data všech vláken o aktuálním stavu zdrojů.

Základní synchronizační nástroje

vzájemné vyloučení (vzájemné vyloučení, zkráceně mutex) – „vlajka“, která směřuje k proudu, který v tento moment má právo pracovat se sdílenými zdroji. Zabraňuje ostatním vláknům v přístupu k obsazené oblasti paměti. V aplikaci může být více než jeden mutex a lze je sdílet mezi procesy. Má to háček: mutex nutí aplikaci pokaždé přistupovat k jádru operačního systému, což je drahé.

Semafor - umožňuje omezit počet vláken, která mají v daný čas přístup ke zdroji. Tím se sníží zatížení procesoru při provádění kódu tam, kde jsou úzká místa. Problém je v tom, že optimální počet vláken závisí na počítači uživatele.

událost - definujete podmínku, po které se řízení přenese na požadované vlákno. Vlákna si vyměňují data událostí, aby se vyvíjeli a logicky pokračovali ve vzájemných akcích. Jeden data obdržel, druhý zkontroloval jejich správnost, třetí je uložil HDD. Události se liší ve způsobu jejich zrušení. Pokud potřebujete upozornit na událost více vláken, budete muset ručně nastavit funkci zrušení pro zastavení signálu. Pokud existuje pouze jeden cílový stream, můžete vytvořit událost s automatickým resetem. Jakmile signál dosáhne streamu, sám to zastaví. Pro flexibilní řízení toku lze události řadit do fronty.

kritický úsek - složitější mechanismus, který kombinuje počítadlo smyček a semafor. Počítadlo umožňuje odložit start semaforu o požadovanou dobu. Výhodou je, že jádro se vyvolá pouze v případě, že je oddíl zaneprázdněn a je třeba zapnout semafor. Zbytek času vlákno běží v uživatelském režimu. Bohužel, sekci lze použít pouze v rámci jednoho procesu.

Jak implementovat multithreading v Javě

Třída Thread je zodpovědná za práci s vlákny v Javě. Vytvořit nové vlákno pro provedení úlohy znamená vytvořit instanci třídy Thread a přidružit ji k požadovanému kódu. Můžete to udělat dvěma způsoby:

    vytvořit podtřídu z vlákna;

    implementujte rozhraní Runnable do vaší třídy a poté předejte instance třídy konstruktoru Thread.

I když se nebudeme dotýkat tématu deadlocks (deadlock), kdy si vlákna vzájemně blokují práci a visí, to si necháme na příští článek.Nyní přejdeme k praxi.

Příklad multithreadingu v Javě: ping pong s mutexy

Pokud si myslíte, že se stane něco hrozného, ​​nadechněte se. Práci se synchronizačními objekty zvážíme téměř hravě: mutex „ohm“ vyhodí dvě vlákna. Ale ve skutečnosti uvidíte skutečnou aplikaci, kde může veřejná data zpracovávat pouze jedno vlákno.

Nejprve vytvořte třídu, která zdědí vlastnosti vlákna, které již známe, a napište metodu „kickBall“:

Veřejná třída PingPongThread rozšiřuje vlákno( PingPongThread(název řetězce)( this.setName(název); // přepíše název vlákna ) @Override public void run() ( Ball ball = Ball.getBall(); while(ball.isInGame()) ( kickBall(ball); ) ) private void kickBall(Ball ball) ( if(!ball.getSide().equals(getName()))( ball.kick(getName()); ) ) )

Nyní se postaráme o míč. Nebude to jednoduché, ale zapamatovatelné: aby věděl, kdo ho udeřil, z jaké strany a kolikrát. K tomu používáme mutex: bude shromažďovat informace o práci každého z vláken – to umožní izolovaným vláknům vzájemně komunikovat. Po 15. úderu vyřadíme míč ze hry, abychom ho moc nezranili.

Veřejná třída Míč ( private int kicks = 0; private static Ball instance = new Ball(); private String side = ""; private Ball()() static Ball getBall()( return instance; ) synchronized void kick (String playername) ( kicks++; strana = jméno hráče; System.out.println(kopy + " " + strana); ) String getSide()( návratová strana; ) boolean isInGame()( return (kopy< 15); } }

A nyní na scénu vstupují dvě hráčská vlákna. Nazvěme je bez dalších řečí Ping and Pong:

Veřejná třída PingPongGame ( PingPongThread player1 = new PingPongThread("Ping"); PingPongThread player2 = new PingPongThread("Pong"); Ball ball; PingPongGame()( ball = Ball.getBall(); ) void startGame() (hází hráč1 Přerušeno .start(); player2.start(); ))

"Stadion je plný lidí - je čas začít zápas." Pojďme oficiálně oznámit zahájení setkání - v hlavní třídě aplikace:

Veřejná třída PingPong ( public static void main(String args) vyvolá InterruptedException ( hra PingPongGame = new PingPongGame(); game.startGame(); ) )

Jak vidíte, není zde nic zběsilého. Toto je jen úvod do multithreadingu, ale už víte, jak to funguje a můžete experimentovat – omezovat dobu trvání hry ne počtem zásahů, ale například časem. Vrátíme se k tématu multithreadingu - zvážíme balíček java.util.concurrent, knihovnu Akka a volatilní mechanismus. A pojďme se bavit o implementaci multithreadingu v Pythonu.

E tento článek není pro ostřílené krotitele Pythonů, pro které je rozplétání této změti hadů dětskou hrou, ale spíše povrchní přehled možností multithreadingu pro nové závisláky na pythonech.

Bohužel v ruštině není tolik materiálu na téma multithreading v Pythonu a se záviděníhodnou pravidelností mi začali narážet pythoneři, kteří třeba o GIL nic neslyšeli. V tomto článku se pokusím popsat nejzákladnější vlastnosti vícevláknové pythonu, řeknu vám, co je GIL a jak s ním (nebo bez něj) žít a mnoho dalšího.


Python je okouzlující programovací jazyk. Dokonale kombinuje mnoho programovacích paradigmat. Většina úloh, se kterými se programátor může setkat, je zde vyřešena jednoduše, elegantně a výstižně. Ale pro všechny tyto úkoly často stačí jednovláknové řešení a programy s jedním vláknem jsou obvykle předvídatelné a snadno laditelné. Co se nedá říci o vícevláknových a víceprocesových programech.

Vícevláknové aplikace


Python má modul závitování a má vše, co potřebujete pro vícevláknové programování: také má jiný druh zámky a semafor a mechanismus událostí. Jedno slovo – vše, co je potřeba pro velkou většinu vícevláknových programů. Navíc použití všech těchto nástrojů je docela jednoduché. Zvažte příklad programu, který spouští 2 vlákna. Jedno vlákno píše deset "0", druhé - deset "1" a přísně v pořádku.

importování závitů

def spisovatel

pro i v xrange(10):

tisknout x

Event_for_set.set()

# init události

e1 = threading.Event()

e2 = threading.Event()

# init vláken

0, e1, e2))

1, e2, e1))

# zahájení vláken

t1.start()

t2.start()

t1.join()

t2.join()


Žádná magie, žádný kód voodoo. Kód je jasný a konzistentní. Navíc, jak vidíte, vytvořili jsme vlákno z funkce. Pro malé úkoly je to velmi výhodné. Tento kód je také poměrně flexibilní. Předpokládejme, že máme 3. proces, který zapíše „2“, pak bude kód vypadat takto:

importování závitů

def spisovatel (x, event_for_wait, event_for_set):

pro i v xrange(10):

Event_for_wait.wait() # čekat na událost

Event_for_wait.clear() # čistá událost pro budoucnost

tisknout x

Event_for_set.set() # nastavit událost pro sousední vlákno

# init události

e1 = threading.Event()

e2 = threading.Event()

e3 = threading.Event()

# init vláken

t1 = threading.Thread(target=writer, args=( 0, e1, e2))

t2 = threading.Thread(target=writer, args=( 1, e2, e3))

t3 = threading.Thread(target=writer, args=( 2, e3, e1))

# zahájení vláken

t1.start()

t2.start()

t3.start()

e1.set() # zahájí první událost

# připojit vlákna k hlavnímu vláknu

t1.join()

t2.join()

t3.join()


Přidali jsme novou událost, nové vlákno a mírně jsme změnili parametry
začnou streamy (můžete samozřejmě napsat více společné rozhodnutí pomocí např. MapReduce, ale to již přesahuje rámec tohoto článku).
Jak vidíte, stále neexistuje žádná magie. Vše je jednoduché a přehledné. Pojďme dále.

Zámek globálního tlumočníka


Existují dva nejčastější důvody, proč používat vlákna: za prvé, zvýšit efektivitu používání vícejádrové architektury moderních procesorů, a tím i výkon programu;
za druhé, pokud potřebujeme rozdělit logiku programu do paralelních plně nebo částečně asynchronních sekcí (například abychom mohli pingnout několik serverů současně).

V prvním případě se potýkáme s takovým omezením Pythonu (nebo spíše jeho hlavní implementace, CPythonu), jako je Global Interpreter Lock (nebo zkráceně GIL). Koncept GIL spočívá v tom, že na procesoru může být současně spuštěno pouze jedno vlákno. Děje se tak proto, aby mezi vlákny nedocházelo k boji o jednotlivé proměnné. Spustitelné vlákno má přístup k celému prostředí. Tato vlastnost implementace vláken v Pythonu značně zjednodušuje práci s vlákny a dává určitou bezpečnost vláken (thread safety).

Ale je tu jeden jemný bod: může se zdát, že vícevláknová aplikace poběží přesně stejnou dobu jako jednovláknová, která dělá totéž, nebo za součet doby provádění každého vlákna na CPU. . Zde nás ale čeká jeden nepříjemný efekt. Zvažte program:

s open("test1.txt" , "w" ) jako fout:

pro i v xrange(1000000):

tisk >> fout, 1


Tento program jednoduše zapíše milion "1" řádků do souboru a udělá to za ~0,35 sekundy na mém počítači.

Zvažte jiný program:

z threading import Thread

def spisovatel(název souboru, n):

s open(název souboru, "w" ) jako fout:

pro i v xrange(n):

tisk >> fout, 1

t1 = vlákno(target=writer, args=("test2.txt" , 500 000,))

t2 = vlákno(cíl=zapisovatel, argumenty=("test3.txt" , 500 000,))

t1.start()

t2.start()

t1.join()

t2.join()


Tento program vytvoří 2 vlákna. V každém streamu zapíše půl milionu řádků „1“ do samostatného souboru. Ve skutečnosti je množství práce stejné jako u předchozího programu. Ale postupem času se zde získá zajímavý efekt. Program může běžet od 0,7 sekundy do 7 sekund. Proč se tohle děje?

To je způsobeno skutečností, že když vlákno nepotřebuje prostředky CPU, uvolní GIL a v tuto chvíli se může pokusit získat jej a další vlákno a také hlavní vlákno. V čem operační systém, s vědomím, že existuje mnoho jader, může vše zhoršit pokusem o distribuci vláken mezi jádra.

UPD: v současné době je v Pythonu 3.2 vylepšená implementace GIL, ve které je tento problém částečně vyřešen, zejména díky tomu, že každé vlákno po ztrátě kontroly čeká krátkou dobu, než může opět zachytit GIL (existují dobré prezentace v angličtině

"Takže nemůžete psát efektivní vícevláknové programy v Pythonu?" ptáte se. Ne, samozřejmě, existuje cesta ven, a dokonce několik.

Víceprocesové aplikace


Abychom trochu vyřešili problém popsaný v předchozím odstavci, má Python modul podproces . Můžeme napsat program, který chceme spustit v paralelním vláknu (ve skutečnosti již proces). A spusťte jej v jednom nebo více vláknech v jiném programu. Tímto způsobem by se náš program opravdu zrychlil, protože vlákna vytvořená v GIL launcheru se nenabírají, ale pouze čekají na ukončení běžícího procesu. S touto metodou je však spojeno mnoho problémů. Hlavním problémem je, že je obtížné přenášet data mezi procesy. Bylo by nutné nějak serializovat objekty, navázat komunikaci přes PIPE nebo jiné nástroje, ale to vše nevyhnutelně vyžaduje režii a kód se stává obtížně srozumitelným.

Zde můžeme použít jiný přístup. Python má modul pro více zpracování . Funkčně se tento modul podobá závitování . Z běžných funkcí lze například stejným způsobem vytvářet procesy. Metody práce s procesy jsou téměř stejné jako u vláken z modulu vláken. Ale pro synchronizaci procesů a výměnu dat je obvyklé používat jiné nástroje. Hovoříme o frontách (Queue) a kanálech (Pipe). Existují však také analogy zámků, událostí a semaforů, které byly ve vláknech.

Multiprocessingový modul má navíc mechanismus pro práci se sdílenou pamětí. K tomu má modul třídy proměnných (Value) a pole (Array), které lze „zobecnit“ (sdílet) mezi procesy. Pro usnadnění práce se sdílenými proměnnými můžete použít třídy manažerů (Manager). Jsou pružnější a snadno se s nimi manipuluje, ale pomaleji. Nemluvě o pěkné možnosti sdílení typů z modulu ctypes pomocí modulu multiprocessing.sharedctypes.

Modul multiprocessingu má také mechanismus pro vytváření poolů procesů. Tento mechanismus je velmi užitečný pro implementaci vzoru Master-Worker nebo pro implementaci paralelní mapy (což je v jistém smyslu speciální případ Master-Worker).

Z hlavních problémů práce s modulem multiprocessing stojí za zmínku relativní platformová závislost tohoto modulu. Protože je práce s procesy v různých operačních systémech organizována odlišně, jsou na kód uvalena určitá omezení. Například ve Windows neexistuje žádný vidlicový mechanismus, takže bod oddělení procesu musí být zabalen do:

if __name__ == "__main__" :


Tento design je však již dobrou formou.

Co jiného...


Na psaní paralelní aplikace V Pythonu existují další knihovny a přístupy. Můžete například použít Hadoop+Python nebo různé implementace MPI v Pythonu (pyMPI, mpi4py). Můžete dokonce použít obaly pro existující knihovny C++ nebo Fortran. Zde bylo možné zmínit takové frameworky / knihovny jako Pyro, Twisted, Tornado a mnoho dalších. Ale to vše je nad rámec tohoto článku.

Pokud se vám můj styl líbil, pak se vám v příštím článku pokusím říct, jak psát jednoduché interprety v PLY a k čemu se dají použít.

Dřívější příspěvky pojednávaly o multithreadingu ve Windows pomocí CreateThread a dalších WinAPI a multithreadingu v Linuxu a dalších *nix systémech pomocí pthreads . Pokud píšete v C++ 11 nebo novějším, máte přístup k std::thread a dalším vícevláknovým primitivům zavedeným v tomto jazykovém standardu. Následující text ukáže, jak s nimi pracovat. Na rozdíl od WinAPI a pthreads je kód napsaný pomocí std::thread multiplatformní.

Poznámka: Výše uvedený kód byl testován na GCC 7.1 a Clang 4.0 pod Arch Linuxem, GCC 5.4 a Clang 3.8 pod Ubuntu 16.04 LTS, GCC 5.4 a Clang 3.8 pod FreeBSD 11 a Visual Studio Community 2017 pod Windows 10. CMake nemůže před verzí 3. mluvit kompilátor k použití standardu C++17 uvedeného ve vlastnostech projektu. Jak nainstalovat CMake 3.8 na Ubuntu 16.04. Aby bylo možné kód zkompilovat pomocí Clang, musí být na systémech *nix nainstalován balíček libc++. Pro Arch Linux balíček je k dispozici na AUR. Ubuntu má balíček libc++-dev, ale můžete narazit na , díky čemuž se kód nestaví tak snadno. Řešení je popsáno na StackOverflow. Na FreeBSD je potřeba nainstalovat balíček cmake-modules pro kompilaci projektu.

Mutexy

Níže je nejjednodušší příklad pomocí vláken a mutexů:

#zahrnout
#zahrnout
#zahrnout
#zahrnout

std::mutex mtx;
statický int čítač = 0 ;


pro (;; ) (
{
std::lock_guard< std:: mutex >lock(mtx) ;

přestávka ;
int ctr_val = ++ čítač;
std::out<< "Thread " << tnum << ": counter = " <<
ctr_val<< std:: endl ;
}

}
}

int main() (
std::vektor< std:: thread >vlákna;
for (int i = 0; i< 10 ; i++ ) {


}

// zde nelze použít const auto&, protože .join() není označeno const

thr.join();
}

std::cout<< "Done!" << std:: endl ;
návrat 0;
}

Všimněte si zabalení std::mutex ve std::lock_guard podle RAII idiomu. Tento přístup zajišťuje, že mutex bude uvolněn při ukončení rozsahu v každém případě, včetně případů, kdy nastanou výjimky. Pro zachycení několika mutexů najednou, aby se zabránilo uváznutí, existuje třída std::scoped_lock . Objevil se však pouze v C++17, a proto nemusí fungovat všude. Pro dřívější verze C++ existuje šablona std::lock, která má podobnou funkčnost, i když ke správnému uvolnění zámků přes RAII vyžaduje další kód.

R.W.Lock

Často dochází k situaci, kdy k přístupu k objektu dochází častěji pro čtení než pro zápis. V tomto případě je místo obvyklého mutexu efektivnější použít zámek pro čtení a zápis, neboli RWLock. RWLock může být zachycen více čtenými vlákny najednou nebo pouze jedním vláknem zápisu. RWLock v C++ odpovídá třídám std::shared_mutex a std::shared_timed_mutex:

#zahrnout
#zahrnout
#zahrnout
#zahrnout

// std::shared_mutex mtx; // nebude fungovat s GCC 5.4
std::shared_timed_mutex mtx;

statický int čítač = 0 ;
static const int MAX_COUNTER_VAL = 100 ;

void thread_proc(int tnum) (
pro (;; ) (
{
// viz také std::shared_lock
std::unique_lock< std:: shared_timed_mutex >lock(mtx) ;
if (počítadlo == MAX_COUNTER_VAL)
přestávka ;
int ctr_val = ++ čítač;
std::out<< "Thread " << tnum << ": counter = " <<
ctr_val<< std:: endl ;
}
std::this_thread::sleep_for(std::chrono::miliseconds(10));
}
}

int main() (
std::vektor< std:: thread >vlákna;
for (int i = 0; i< 10 ; i++ ) {
std::thread thr(thread_proc, i) ;
threads.emplace_back(std::move(thr) ) ;
}

pro (auto & thr : vlákna) (
thr.join();
}

std::cout<< "Done!" << std:: endl ;
návrat 0;
}

Analogicky s std::lock_guard se třídy std::unique_lock a std::shared_lock používají k zachycení RWLock v závislosti na tom, jak chceme zachytit zámek. Třída std::shared_timed_mutex byla představena v C++14 a funguje na všech* moderních platformách (nemluvím o mobilních zařízeních, herních konzolích atd.). Na rozdíl od std::shared_mutex má metody try_lock_for, try_lock_unti a další, které se snaží získat mutex v daném čase. Silně se domnívám, že std::shared_mutex by měl být levnější než std::shared_timed_mutex. Nicméně std::shared_mutex se objevil pouze v C++17, což znamená, že není podporován všude. Zejména stále hojně používaný GCC 5.4 o tom neví.

Vlákno Místní úložiště

Někdy je potřeba vytvořit proměnnou, například globální, ale vidí ji pouze jedno vlákno. Ostatní vlákna také vidí proměnnou, ale mají svou vlastní místní hodnotu. Aby toho dosáhli, přišli s Thread Local Storage nebo TLS (nemá nic společného s Transport Layer Security!). Pomocí TLS lze mimo jiné výrazně urychlit generování pseudonáhodných čísel. Příklad použití TLS v C++:

#zahrnout
#zahrnout
#zahrnout
#zahrnout

std::mutex io_mtx;
thread_local int counter = 0 ;
static const int MAX_COUNTER_VAL = 10 ;

void thread_proc(int tnum) (
pro (;; ) (
čítač++ ;
if (počítadlo == MAX_COUNTER_VAL)
přestávka ;
{
std::lock_guard< std:: mutex >lock(io_mtx) ;
std::out<< "Thread " << tnum << ": counter = " <<
čelit<< std:: endl ;
}
std::this_thread::sleep_for(std::chrono::miliseconds(10));
}
}

int main() (
std::vektor< std:: thread >vlákna;
for (int i = 0; i< 10 ; i++ ) {
std::thread thr(thread_proc, i) ;
threads.emplace_back(std::move(thr) ) ;
}

pro (auto & thr : vlákna) (
thr.join();
}

std::cout<< "Done!" << std:: endl ;
návrat 0;
}

Mutex se zde používá výhradně k synchronizaci výstupu do konzole. Pro přístup k proměnným thread_local není nutná žádná synchronizace.

Atomové proměnné

Atomové proměnné se často používají k provádění jednoduchých operací bez použití mutexů. Například potřebujete zvýšit čítač z více vláken. Místo zabalení int do std::mutex je efektivnější použít std::atomic_int. C++ také nabízí std::atomic_char, std::atomic_bool a mnoho dalších typů. Také implementují bezzámkové algoritmy a datové struktury na atomárních proměnných. Je třeba poznamenat, že je velmi obtížné je vyvíjet a ladit a ne všechny systémy pracují rychleji než podobné algoritmy a datové struktury se zámky.

Příklad kódu:

#zahrnout
#zahrnout
#zahrnout
#zahrnout
#zahrnout

static std:: atomic_int atomic_counter(0 ) ;
static const int MAX_COUNTER_VAL = 100 ;

std::mutex io_mtx;

void thread_proc(int tnum) (
pro (;; ) (
{
int ctr_val = ++ atomic_counter;
if (ctr_val >= MAX_COUNTER_VAL)
přestávka ;

{
std::lock_guard< std:: mutex >lock(io_mtx) ;
std::out<< "Thread " << tnum << ": counter = " <<
ctr_val<< std:: endl ;
}
}
std::this_thread::sleep_for(std::chrono::miliseconds(10));
}
}

int main() (
std::vektor< std:: thread >vlákna;

int nthreads = std::thread::hardware_concurrency();
if (nvlákna == 0 ) nvlákna = 2 ;

for (int i = 0; i< nthreads; i++ ) {
std::thread thr(thread_proc, i) ;
threads.emplace_back(std::move(thr) ) ;
}

pro (auto & thr : vlákna) (
thr.join();
}

std::cout<< "Done!" << std:: endl ;
návrat 0;
}

Všimněte si použití procedury hardware_concurrency. Vrací odhadovaný počet vláken, která lze paralelně spustit na aktuálním systému. Například na stroji se čtyřjádrovým procesorem, který podporuje hyper threading, procedura vrátí číslo 8. Procedura může také vrátit nulu, pokud se vyhodnocení nezdaří nebo procedura prostě není implementována.

Některé informace o tom, jak atomové proměnné fungují na úrovni assembleru, naleznete v cheat sheetu se základními instrukcemi pro assembler x86/x64.

Závěr

Jak vidím, vše funguje opravdu dobře. To znamená, že při psaní multiplatformních aplikací v C ++ můžete s klidem zapomenout na WinAPI a pthreads. Pure C má také multiplatformní vlákna od C11. Ale stále je nepodporuje Visual Studio (zkontroloval jsem) a je nepravděpodobné, že nikdy budou. Není žádným tajemstvím, že Microsoft nevidí žádný zájem o rozvoj podpory pro jazyk C ve svém kompilátoru a raději se soustředí na C++.

V zákulisí stále zůstává spousta primitivů: std::condition_variable(_any), std::(shared_)future, std::promise, std::sync a další. Pro seznámení s nimi doporučuji stránky cppreference.com. Také může mít smysl přečíst si knihu C++ Concurrency in Action . Musím vás ale upozornit, že už není novinka, obsahuje hodně vody a v podstatě převypráví tucet článků z cppreference.com.

Plná verze zdrojového kódu této poznámky je jako obvykle na GitHubu. Jak nyní píšete vícevláknové aplikace v C++?

Kapitola 10.

Vícevláknové aplikace

Multitasking v moderních operačních systémech je považován za samozřejmost [ Před příchodem Apple OS X nebyly na počítačích Macintosh žádné moderní multitaskingové operační systémy. Správně navrhnout operační systém s plným multitaskingem je velmi obtížné, takže OS X musel být založen na systému Unix.]. Uživatel očekává, že při současném spuštění textového editoru a e-mailového klienta tyto programy nebudou kolidovat a při příjmu e-mailu editor nepřestane fungovat. Při spouštění více programů současně operační systém rychle přepíná mezi programy a postupně jim dává procesor (pokud samozřejmě nemá počítač nainstalováno více procesorů). V důsledku toho vytváří iluze běží více programů současně, protože ani ten nejlepší písař (a nejrychlejší připojení k internetu) nemůže držet krok s moderním procesorem.

Multithreading lze v jistém smyslu považovat za další úroveň multitaskingu: namísto přepínání mezi různými programy operační systém přepíná mezi různými částmi stejného programu. Například e-mailový klient s více vlákny vám umožňuje přijímat nové e-mailové zprávy při čtení nebo psaní nových zpráv. V dnešní době je multithreading také mnohými uživateli považován za samozřejmost.

VB nikdy neměl normální podporu pro multithreading. Je pravda, že ve VB5 se objevila jedna z jeho odrůd - kolaborativní model streamování(protahování bytů). Jak brzy uvidíte, souběžný model poskytuje programátorovi některé výhody multithreadingu, ale nevyužívá je naplno. Dříve nebo později musíte přejít z tréninkového stroje na skutečný a VB .NET se stal první verzí VB, která podporuje bezplatný vícevláknový model.

Multithreading však není funkce, která by se snadno implementovala v programovacích jazycích a programátoři ji snadno zvládli. Proč?

Protože aplikace s více vlákny mohou mít velmi záludné chyby, které přicházejí a odcházejí nepředvídatelně (a tyto chyby se nejhůře ladí).

Spravedlivé varování: multithreading je jednou z nejtěžších oblastí programování. Sebemenší nepozornost vede ke vzniku nepolapitelných chyb, jejichž oprava vyžaduje astronomické sumy. Z tohoto důvodu obsahuje tato kapitola mnoho špatný příklady - záměrně jsme je napsali tak, aby byly demonstrovány charakteristické chyby. Toto je nejbezpečnější přístup k učení vícevláknového programování: měli byste být schopni vidět potenciální problémy, když se na první pohled zdá, že vše funguje dobře, a vědět, jak je vyřešit. Pokud chcete používat techniky vícevláknového programování, je to nezbytné.

Tato kapitola položí pevný základ pro další samostatnou práci, ale nebudeme schopni popsat vícevláknové programování ve všech jeho jemnostech – pouze tištěná dokumentace o třídách jmenného prostoru Threading zabírá více než 100 stran. Pokud chcete ovládat vícevláknové programování na vyšší úrovni, podívejte se do specializovaných knih.

Ale bez ohledu na to, jak nebezpečné je vícevláknové programování, je pro profesionální řešení některých problémů nepostradatelné. Pokud vaše programy nepoužívají multithreading tam, kde je to vhodné, uživatelé budou velmi zklamáni a dají přednost jinému produktu. Například až ve čtvrté verzi oblíbeného e-mailového programu Eudora se objevily vícevláknové schopnosti, bez kterých si nelze představit žádný moderní e-mailový program. V době, kdy Eudora zavedla podporu multithreadingu, mnoho uživatelů (včetně jednoho z autorů této knihy) přešlo na jiné produkty.

Konečně, jednovláknové programy v .NET prostě neexistují. Všechno Programy .NET jsou vícevláknové, protože garbage collector běží jako proces na pozadí s nízkou prioritou. Jak je ukázáno níže, pro seriózní grafické programování v .NET pomáhá správná komunikace programových vláken zabránit zablokování GUI, když program provádí zdlouhavé operace.

Úvod do multithreadingu

Každý program funguje v určitém kontext, popisující distribuci kódu a dat v paměti. Po uložení kontextu se skutečně uloží stav programového vlákna, což vám umožní jej v budoucnu obnovit a pokračovat v provádění programu.

Ukládání kontextu je spojeno s určitou cenou času a paměti. Operační systém si pamatuje stav programového vlákna a předá řízení jinému vláknu. Když chce program pokračovat ve vykonávání pozastaveného vlákna, musí být obnovený uložený kontext, což zabere ještě více času. Proto by se multithreading měl používat pouze tehdy, když přínosy převažují nad náklady. Některé typické příklady jsou uvedeny níže.

  • Funkčnost programu je přehledně a přirozeně rozdělena do několika heterogenních operací, jako v příkladu přijímání e-mailů a příprava nových zpráv.
  • Program provádí dlouhé a složité výpočty a nechcete, aby během výpočtů bylo blokováno grafické rozhraní.
  • Program běží na víceprocesorovém počítači s operačním systémem, který podporuje použití více procesorů (pokud počet aktivních vláken nepřekročí počet procesorů, nestojí paralelní provádění téměř žádné náklady na přepínání vláken).

Než přejdeme k mechanice fungování vícevláknových programů, je nutné upozornit na jednu okolnost, která mezi začátečníky v oblasti vícevláknového programování často způsobuje zmatek.

Programové vlákno provede proceduru, nikoli objekt.

Těžko říct, co je míněno slovním spojením "objekt se provádí", ale jeden z autorů často vede semináře o vícevláknovém programování a tato otázka je pokládána častěji než ostatní. Někdo by si mohl myslet, že vlákno programu začíná voláním metody New třídy, po které vlákno zpracovává všechny zprávy předané odpovídajícímu objektu. Takové reprezentace Absolutně jsou špatné. Jeden objekt může obsahovat několik vláken provádějících různé (a někdy dokonce stejné) metody, zatímco zprávy o objektech jsou přenášeny a přijímány několika různými vlákny (mimochodem, to je jeden z důvodů, proč je programování s více vlákny obtížné: za účelem ladění programu, musíte vědět, které vlákno v daném okamžiku provádí tu či onu proceduru!).

Protože vlákna jsou vytvářena na základě objektových metod, samotný objekt je obvykle vytvořen před vláknem. Po úspěšném vytvoření objektu program vytvoří vlákno, předá mu adresu metody objektu a teprve potom dá vláknu pokyn ke spuštění. Procedura, pro kterou bylo vlákno vytvořeno, stejně jako všechny procedury, může vytvářet nové objekty, provádět operace s existujícími objekty a volat další procedury a funkce, které jsou v jejím oboru.

Programová vlákna mohou také spouštět metody sdílené třídy. V tomto slu-Mějte také na paměti další důležitou okolnost: vlákno končí výstupem z procedury, pro kterou bylo vytvořeno. Až do ukončení procedury je normální dokončení programového vlákna nemožné.

Vlákna mohou končit nejen přirozeně, ale také abnormálně. To se obvykle nedoporučuje. Další informace naleznete v části Ukončení a přerušení vláken.

Základní funkce .NET související s používáním vláken jsou umístěny ve jmenném prostoru Threading. Většina programů s více vlákny by proto měla začínat následujícím řádkem:

Importy System.Threading

Import jmenného prostoru zjednodušuje zadávání programu a umožňuje používat technologii IntelliSense.

Přímé spojení vláken s procedurami naznačuje, že na tomto obrázku zaujímá důležité místo delegáti(viz kapitola 6). Konkrétně jmenný prostor Threading obsahuje delegáta ThreadStart, který se běžně používá při spouštění programových vláken. Syntaxe pro použití tohoto delegáta vypadá takto:

Public Delegate Sub ThreadStart()

Kód volaný delegátem ThreadStart nesmí mít parametry ani návratovou hodnotu, takže nelze vytvořit vlákna pro funkce (které vracejí hodnotu) a procedury s parametry. Chcete-li předat informace ze streamu, musíte také hledat alternativní prostředky, protože prováděné metody nevrací hodnoty a nemohou používat pass-by-reference. Pokud je například procedura ThreadMethod ve třídě WilluseThread, může ThreadMethod předávat informace změnou vlastností instancí třídy WillUseThread.

Aplikační domény

Programová vlákna .NET běží v takzvaných aplikačních doménách, definovaných v dokumentaci jako „izolované prostředí, ve kterém běží aplikace“. Aplikační doménu si můžete představit jako odlehčenou verzi procesů Win32; jeden proces Win32 může obsahovat více aplikačních domén. Hlavní rozdíl mezi aplikačními doménami a procesy je ten, že proces Win32 má svůj vlastní adresní prostor (dokumentace také porovnává aplikační domény s logickými procesy běžícími uvnitř fyzického procesu). V .NET se o veškerou správu paměti stará runtime, takže ve stejném procesu Win32 může běžet více aplikačních domén. Jednou z výhod tohoto schématu je zlepšení možností škálování aplikací. Nástroje pro práci s aplikačními doménami jsou ve třídě AppDomain. Doporučujeme přečíst si dokumentaci k této třídě. S ním můžete získat informace o prostředí, ve kterém váš program běží. Třída AppDomain se používá zejména při reflexi tříd systému .NET. Následující program uvádí seznam načtených sestav.

Import System.Reflection

Modul

SubMain()

Dim theDomain as AppDomain

theDomain = AppDomain.CurrentDomain

Dim Assemblies()As

Sestavení = theDomain.GetAssemblies

Dim anAssemblyxAs

Pro každou sestavu v sestavách

Console.WriteLinetanAssembly.Full Name) Další

Console.ReadLine()

konec sub

Koncový modul

Vytváření vláken

Začněme elementárním příkladem. Řekněme, že chcete spustit proceduru v samostatném vláknu, které snižuje čítač v nekonečné smyčce. Postup je definován jako součást třídy:

Veřejná třída WillUseThreads

Public SubtractFromCounter()

Dim count As Integer

Udělejte, dokud je pravda počet -= 1

Console.WriteLlne("Jsem v jiném vláknu a čítam ="

&počet)

smyčka

konec sub

závěrečná třída

Protože podmínka cyklu Do je vždy pravdivá, můžete si myslet, že nic nezabrání provedení procedury SubtractFromCounter. To však není vždy případ vícevláknové aplikace.

Následující úryvek ukazuje proceduru Sub Main, která spouští vlákno, a příkaz Imports:

Možnost Strict On Imports System.Threading Module Module

SubMain()

1 ztlumit můj test jako nový WillUseThreads()

2 Dim bThreadStart jako nový ThreadStart (AddressOf _

myTest.SubtractFromCounter)

3 ztlumit bThread jako nové vlákno (bThreadStart)

4" bThread.Start()

Dim i As Integer

5 Dělejte, dokud je pravda

Console.WriteLine("V hlavním vláknu a počet je " & i) i += 1

smyčka

konec sub

Koncový modul

Pojďme se postupně podívat na nejdůležitější body. Nejprve vždy funguje procedura Sub Man n hlavní proud(hlavní vlákno). V programech .NET jsou vždy spuštěny alespoň dvě vlákna: hlavní vlákno a vlákno garbage collection. Řádek 1 vytvoří novou instanci testovací třídy. Na řádku 2 vytvoříme delegáta ThreadStart a předáme adresu procedury SubtractFromCounter instance testovací třídy vytvořené na řádku 1 (tato procedura se volá bez parametrů). Dobrýzadávání dlouhého názvu importu jmenného prostoru Threading lze vynechat. Nový objekt vlákna je vytvořen na řádku 3. Všimněte si předání delegáta ThreadStart při volání konstruktoru třídy Thread. Někteří programátoři dávají přednost spojení těchto dvou řádků do jednoho logického řádku:

Dim bThread As New Thread(New ThreadStarttAddressOf _

myTest.SubtractFromCounter))

Nakonec řádek 4 „spustí“ vlákno voláním metody Start instance Thread vytvořené pro delegáta ThreadStart. Voláním této metody indikujeme operačnímu systému, že procedura Subtract by měla běžet na samostatném vláknu.

Slovo „startuje“ v předchozím odstavci je uzavřeno v uvozovkách, protože v tomto případě nastává jedna z mnoha zvláštností vícevláknového programování: volání Start ve skutečnosti vlákno nespustí! Pouze říká, že operační systém by měl naplánovat spuštění zadaného vlákna, ale jeho přímé spuštění je mimo kontrolu programu. Vlákna nebudete moci spouštět sami, protože spouštění vláken vždy řídí operační systém. V pozdější části se dozvíte, jak používat prioritu, aby operační systém zrychlil vaše vlákno.

Na Obr. Obrázek 10.1 ukazuje příklad toho, co se může stát po spuštění programu a jeho přerušení klávesou Ctrl+Break. V našem případě se nové vlákno spustilo až poté, co se počítadlo v hlavním vláknu zvýšilo na 341!

Rýže. 10.1. Jednoduchý vícevláknový programově běžící čas

Pokud program běží delší dobu, bude výsledek vypadat podobně jako na obr. 10.2. Vidíme, že vyprovádění běžícího vlákna je pozastaveno a řízení je opět přeneseno na hlavní vlákno. V tomto případě dochází k projevu preemptivní multithreading prostřednictvím krájení času. Význam tohoto děsivého termínu je vysvětlen níže.

Rýže. 10.2. Přepínání mezi vlákny v jednoduchém vícevláknovém programu

Při přerušování vláken a předávání řízení jiným vláknům operační systém využívá principu preemptivního multithreadingu pomocí time slicingu. Časové dělení také řeší jeden z běžných problémů, které se vyskytovaly ve vícevláknových programech – jedno vlákno zabírá veškerý čas CPU a nepředává řízení jiným vláknům (to se obvykle děje v intenzivních smyčkách, jako je výše uvedené). Aby se zabránilo výhradnímu převzetí procesoru, měla by vaše vlákna čas od času přenést řízení na jiná vlákna. Pokud se ukáže, že je program „nevědomý“, existuje další, o něco méně žádoucí řešení: operační systém vždy předběžně zakáže běžící vlákno, bez ohledu na úroveň jeho priority, takže přístup k procesoru je udělen každému vláknu v Systém.

Protože schémata dělení všech verzí Windows s .NET přidělují každému vláknu minimální množství času, problémy s vlastnictvím CPU nejsou v programování .NET tak závažné. Na druhou stranu, pokud bude prostředí .NET někdy přizpůsobeno pro jiné systémy, může se situace změnit.

Pokud před voláním Start zahrneme do našeho programu následující řádek, pak i vlákna s nejnižší prioritou získají určitý čas CPU:

bThread.Priority = ThreadPriority.Highest

Rýže. 10.3. Vlákno s nejvyšší prioritou se obvykle spouští rychleji

Rýže. 10.4. Procesor je také přidělen vláknům s nižší prioritou.

Příkaz přiřadí maximální prioritu novému vláknu a sníží prioritu hlavního vlákna. Z Obr. Obrázek 10.3 ukazuje, že nové vlákno začíná běžet rychleji než dříve, ale jak ukazuje obrázek 10-3. 10.4 získává řízení také hlavní vlákno(ovšem na velmi krátkou dobu a až po dlouhé práci proudu s odečítáním). Když program spustíte na svých počítačích, získáte výsledky podobné těm, které jsou znázorněny na Obr. 10.3 a 10.4, ale kvůli rozdílům mezi našimi systémy nebude existovat přesná shoda.

Výčtový typ ThreadPrlority obsahuje hodnoty pro pět úrovní priority:

Priorita vlákna. Nejvyšší

ThreadPriority.AboveNormal

ThreadPrlority.Normal

ThreadPriority.BelowNormal

ThreadPriority.Nejnižší

Metoda připojení

Někdy je třeba vlákno programu pozastavit, dokud se nedokončí jiné vlákno. Řekněme, že chcete pozastavit vlákno 1, dokud vlákno 2 nedokončí výpočet. Pro tohle ze streamu 1 je volána metoda Join pro vlákno 2. Jinými slovy, příkaz

vlákno2.Join()

pozastaví aktuální vlákno a čeká na dokončení vlákna 2. Vlákno 1 přejde na uzamčený stav.

Pokud spojíte vlákno 1 s vláknem 2 pomocí metody Join, operační systém automaticky spustí vlákno 1 po ukončení vlákna 2. Pamatujte, že proces spouštění je nedeterministický: neexistuje způsob, jak přesně říct, jak dlouho po skončení vlákna 2 začne fungovat vlákno 1. Existuje další verze Join, která vrací booleovskou hodnotu:

vlákno2.Join(celé číslo)

Tato metoda buď čeká na dokončení vlákna 2, nebo odblokuje vlákno 1 po uplynutí zadaného časového intervalu, což způsobí, že plánovač operačního systému znovu přidělí vláknu čas procesoru. Metoda vrátí True, pokud vlákno 2 skončí před vypršením zadaného časového limitu, a False jinak.

Nezapomeňte na základní pravidlo: zda je vlákno 2 ukončeno nebo vypršel časový limit, nemůžete ovlivnit, kdy je vlákno 1 aktivováno.

Názvy vláken, CurrentThread a ThreadState

Vlastnost Thread.CurrentThread vrací odkaz na aktuálně spouštěný objekt vlákna.

Přestože má VB .NET skvělé okno Threads pro ladění vícevláknových aplikací, které je popsáno níže, často nás zachránil příkaz

MsgBox(Thread.CurrentThread.Name)

Často se ukázalo, že kód je spouštěn ve zcela jiném vlákně, ve kterém měl být vykonán.

Připomeňme, že pojem „nedeterministické plánování programových vláken“ znamená velmi jednoduchou věc: programátor prakticky nemá k dispozici žádné prostředky, jak ovlivnit práci plánovače. Z tohoto důvodu programy často používají vlastnost ThreadState, která vrací informace o aktuálním stavu vlákna.

Okno vláken

Okno Threads Visual Studio .NET je neocenitelným pomocníkem při ladění vícevláknových programů. Aktivuje se příkazem podnabídky Debug > Windows v režimu přerušení. Řekněme, že jste vlákno pojmenovali bThread pomocí následujícího příkazu:

bThread.Name = "Odečítání vlákna"

Přibližný pohled na okno vlákna po přerušení programu kombinací kláves Ctrl+Break (nebo jiným způsobem) je na Obr. 10.5.

Rýže. 10.5. Okno vláken

Šipka v prvním sloupci označuje aktivní vlákno vrácené vlastností Thread.CurrentThread. Sloupec ID obsahuje číselná ID vlákna. V dalším sloupci jsou uvedeny názvy vláken (pokud byly nějaké přiřazeny). Sloupec Location označuje proceduru, která má být provedena (například procedura WriteLine třídy Console na obrázku 10-5). Zbývající sloupce obsahují informace o prioritě a pozastavených vláknech (viz další část).

Okno vláken (nikoli operační systém!) vám umožňuje ovládat vlákna vašeho programu pomocí kontextových nabídek. Aktuální vlákno můžete například zastavit kliknutím na příslušný řádek klikněte pravým tlačítkem myši myši a vyberte příkaz Zmrazit (později lze práci zastaveného vlákna obnovit). Zastavení vláken se často používá při ladění, aby špatně se chovající vlákno nerušilo aplikaci. Okno vláken navíc umožňuje aktivovat další (nezastavené) vlákno; Chcete-li to provést, klepněte pravým tlačítkem myši na požadovaný řádek a vyberte v kontextová nabídka příkaz Přepnout na vlákno (nebo stačí dvakrát kliknout na čáru vlákna). Jak bude ukázáno později, je to velmi užitečné při diagnostice potenciálních uváznutí.

Pozastavení vlákna

Dočasně nepoužívaná vlákna lze uvést do pasivního stavu pomocí metody spánku. Pasivní vlákno je také považováno za zablokované. Samozřejmě s převedením vlákna do pasivního stavu získá podíl zbývajících vláken více procesorových prostředků. Standardní syntaxe metody Sleeper je následující: Thread.Sleep(interval_in_miliseconds)

Volání režimu spánku způsobí, že se aktivní vlákno stane neaktivním alespoň po zadaný počet milisekund (není však zaručeno, že se probudí ihned po uplynutí zadaného intervalu). Všimněte si, že při volání metody není předán žádný odkaz na konkrétní vlákno - metoda spánku je volána pouze pro aktivní vlákno.

Jiná verze spánku způsobí, že aktuální vlákno poskytne zbytek přiděleného času CPU:

Thread.Sleep(0)

Následující možnost uvede aktuální vlákno do pasivního stavu na neomezenou dobu (aktivace nastane pouze při volání přerušení):

Thread.Sleer(Timeout.Infinite)

Protože pasivní vlákna (i s neomezenými časovými limity) mohou být přerušena metodou Interrupt, což způsobí vyvolání ThreadInterruptExceptionException, je volání Slayer vždy zabaleno do bloku Try-Catch, jako v následujícím úryvku:

Snaž se

Thread.Sleep(200)

" Stav pasivního vlákna byl přerušen

Catch e As Exception

„Další výjimky

Ukončit pokus

Každý .NET program běží na programovém vláknu, proto se metoda Sleep používá také k pozastavení programů (pokud program neimportuje jmenný prostor Threadipg, musíte použít plně kvalifikovaný název Threading.Thread.Sleep).

Ukončení nebo přerušení programových vláken

Vlákno je automaticky ukončeno při ukončení metody určené při vytvoření delegáta ThreadStart, ale někdy je žádoucí ukončit metodu (potažmo vlákno), když nastanou určité faktory. V takových případech toky obvykle kontrolují stavová proměnná v závislosti na stavuje rozhodnuto o nouzovém výstupu z toku. Pro tento účel je do postupu zpravidla zahrnuta smyčka Do-While:

Sub ThreadedMethod()

„Program musí poskytovat prostředky pro průzkum

"proměnná podmínek.

" Například proměnnou podmínky lze zapsat jako vlastnost

Do While conditionVariable = False And MoreWorkToDo

"Hlavní kód

Loop End Sub

Dotazování proměnné podmínky nějakou dobu trvá. Neustálé dotazování ve smyčce by se mělo používat pouze v případě, že očekáváte předčasné ukončení vlákna.

Pokud se test proměnné podmínky musí uskutečnit na určitém místě, použijte příkaz If-Then v kombinaci s Exit Sub v nekonečné smyčce.

Přístup k podmínkové proměnné je třeba synchronizovat, aby ostatní vlákna nenarušovala její normální použití. Toto důležité téma je popsáno v části Odstraňování problémů: Synchronizace.

Bohužel kód pasivních (nebo jinak zablokovaných) vláken není spuštěn, takže dotazování podmíněné proměnné pro ně není volbou. V tomto případě byste měli volat metodu přerušení na objektové proměnné obsahující odkaz na požadované vlákno.

Metodu přerušení lze volat pouze u vláken, která jsou ve stavu čekání, spánku nebo připojení. Pokud zavoláte přerušení u vlákna, které je v jednom z těchto stavů, po chvíli se vlákno znovu spustí a běhové prostředí vyvolá na vlákno výjimku ThreadInterruptedException. K tomu dochází i v případě, že vlákno bylo uvedeno do režimu spánku na dobu neurčitou voláním Thread.Sleepdimeout. nekonečný). Říkáme „po nějaké době“, protože plánování vláken je nedeterministické povahy. Výjimka ThreadInterruptedException při výjimce je zachycena sekcí Catch obsahující kód ukončení ze stavu čekání. Sekce Catch však není vyžadována k ukončení vlákna voláním přerušení – vlákno nakládá s výjimkou, jak uzná za vhodné.

V .NET lze metodu přerušení volat i na neblokujících vláknech. V tomto případě se vlákno přeruší na dalším bloku.

Pozastavení a zabití vláken

Jmenný prostor Threading obsahuje další metody, které přerušují normální fungování vláken:

  • Pozastavit;
  • potrat.

Těžko říct, proč .NET zahrnul podporu pro tyto metody – volání Suspend a Abort s největší pravděpodobností způsobí nestabilitu programu. Žádná z metod neumožňuje správně deinicializovat vlákno. Navíc, když je voláno Suspend nebo Abort, není možné předpovědět, v jakém stavu vlákno zanechá objekty po pozastavení nebo přerušení.

Volání Abort má za následek vyvolání ThreadAbortException. Abychom vám pomohli pochopit, proč by tato podivná výjimka neměla být zpracována v programech, zde je výňatek z dokumentace .NET SDK:

„...Když je vlákno zničeno voláním Abort, běhové prostředí vyvolá ThreadAbortException. Toto je zvláštní druh výjimky, kterou program nemůže zachytit. Když je vyvolána tato výjimka, běhové prostředí provede všechny bloky Final před ukončením vlákna. Protože nakonec bloky mohou dělat cokoliv, zavolejte Join, abyste se ujistili, že je vlákno zrušeno."

Moral: Abort and Suspend se nedoporučují (a pokud se stále nemůžete obejít bez Suspend, obnovte pozastavené vlákno pomocí metody Resume). Jediným způsobem, jak bezpečně ukončit vlákno, je dotazování synchronizované proměnné podmínky nebo volání výše popsané metody přerušení.

Vlákna na pozadí (démoni)

Některá vlákna běžící na pozadí se automaticky ukončí, když se zastaví jiné součásti programu. Zejména garbage collector běží na jednom z vláken na pozadí. Obvykle jsou vlákna na pozadí vytvořena pro příjem dat, ale to se provádí pouze v případě, že na jiných vláknech běží kód, který dokáže zpracovat přijatá data. Syntaxe: název vlákna.IsBackGround = True

Pokud v aplikaci zůstanou pouze vlákna na pozadí, aplikace se automaticky ukončí.

Vážnější příklad: extrahování dat z HTML kódu

Vlákna doporučujeme používat pouze tehdy, když je funkčnost programu přehledně rozdělena do více operací. dobrý příklad je program pro extrakci dat z HTML kódu z kapitoly 9. Naše třída provádí dvě operace: získává data z webu Amazon a zpracovává je. Zde je dokonalý příklad situace, ve které je vícevláknové programování opravdu vhodné. Vytváříme třídy pro několik různých knih a poté analyzujeme data v různých vláknech. Vytvoření nového vlákna pro každou knihu zlepšuje efektivitu programu, protože zatímco jedno vlákno přijímá data (což může vyžadovat čekání na serveru Amazon), jiné vlákno bude zaneprázdněno zpracováním již přijatých dat.

Vícevláknová varianta tohoto programu funguje efektivněji než jednovláknová pouze na počítači s více procesory nebo pokud lze příjem dalších dat efektivně kombinovat s jejich analýzou.

Jak bylo uvedeno výše, ve vláknech mohou běžet pouze procedury, které nemají žádné parametry, takže budete muset v programu provést malé změny. Následuje hlavní postup, přepsaný s výjimkou parametrů:

Public Sub FindRank()

m_Rank = ScrapeAmazon()

Console.WriteLine("hodnost " & m_Name & "Is" & GetRank)

konec sub

Protože k ukládání a získávání informací nebudeme moci použít combo box (psaní vícevláknových programů GUI je popsáno v poslední části této kapitoly), program ukládá data čtyř knih do pole, jehož definice začíná jako tento:

Dim theBook(3.1) As String theBook(0.0) = "1893115992"

theBook(0.l) = "Programování VB .NET" " Atd.

Čtyři vlákna jsou vytvořena ve stejném cyklu, který vytváří objekty AmazonRanker:

Pro i= 0, pak 3

Snaž se

theRanker = nový AmazonRanker(theBook(i.0). theBookd.1))

aThreadStart = New ThreadStar(AddressOf theRanker.FindRan()

aThread = Nové vlákno (aThreadStart)

aThread.Name = theBook(i.l)

aThread.Start() Catch e As Exception

Console.WriteLine(e.Message)

Ukončit pokus

další

Níže je celé znění programu:

Možnost Strict On Imports System.IO Importuje System.Net

Importy System.Threading

Modul

SubMain()

Dim theBook(3.1) As String

theBook(0.0) = "1893115992"

theBook(0.l) = "Programování VB .NET"

theBook(l.0) = "1893115291"

theBook(l.l) = "Programování databáze VB .NET"

theBook(2,0) = "1893115623"

theBook(2.1) = "Programátorský úvod do C#."

theBook(3.0) = "1893115593"

theBook(3.1) = "Gland the .Net Platform"

Dim i As Integer

Dim theRanker jako =AmazonRanker

Dim aThreadStart As Threading.ThreadStart

Dim aThread As Threading.Thread

Pro i = 0 až 3

Snaž se

theRanker = New AmazonRankerttheBook (i.0). kniha (i.1))

aThreadStart = New ThreadStart (AddressOf theRanker.FindRank)

aThread = Nové vlákno (aThreadStart)

aThread.Name= theBook(i.l)

aThread.Start()

Catch e As Exception

Console.WriteLlnete.Message)

Konec Zkuste Další

Console.ReadLine()

konec sub

Koncový modul

Veřejná třída AmazonRanker

Private m_URL As String

Private m_Rank As Integer

Private m_Name As String

Public Sub New (ByVal ISBN As String. ByVal theName As String)

m_URL = "http://www.amazon.com/exec/obidos/ASIN/" & ISBN

m_Name = theName End Sub

Public Sub FindRank() m_Rank = ScrapeAmazon()

Console.Writeline("hodnost " & m_Name & "je "

& GetRank) End Sub

Veřejná vlastnost pouze pro čtení GetRank() As String Get

Pokud m_Rank<>0 Pak

Return CStr(m_Rank) Jinak

"Problémy

End If

Konec dostat

Koncová vlastnost

Veřejná vlastnost pouze pro čtení GetName() As String Get

Vraťte m_Name

Konec dostat

Koncová vlastnost

Private Function ScrapeAmazon() As Integer Try

Dim theURL as New Uri(m_URL)

Dim theRequest jako WebRequest

theRequest =WebRequest.Create(theURL)

Dim theResponse as WebResponse

theResponse = theRequest.GetResponse

Dim aReader jako nový StreamReader(theResponse.GetResponseStream())

Dim theData as String

theData = aReader.ReadToEnd

Návratová analýza (data)

Catch E jako výjimka

Console.WriteLine(E.Message)

Console.WriteLine(E.StackTrace)

řídicí panel. ReadLine()

End Try End Function

Private Function Analyze (ByVal theData As String) As Integer

Dim Location As.Integer Location = theData.IndexOf(" Amazon.com

Prodejní pořadí:") _

+ "Prodejní pořadí Amazon.com:".Délka

Dim temp As String

Proveďte, dokud theData.Substring(Location.l) = "<" temp = temp

&theData.Substring(Location.l) Umístění += 1 smyčka

Návrat Clnt (temp)

koncová funkce

závěrečná třída

Vícevláknové operace jsou běžné v jmenných prostorech .NET a I/O, takže knihovna .NET Framework pro ně poskytuje vyhrazené asynchronní metody. Další informace o používání asynchronních metod při psaní programů s více vlákny najdete v metodách BeginGetResponse a EndGetResponse třídy HTTPWebRequest.

Hlavní nebezpečí (obecné údaje)

Dosud byl zvažován jediný bezpečný případ použití pro vlákna - naše vlákna nezměnila sdílená data. Pokud povolíte změny sdílených dat, potenciální chyby se množí exponenciálně a pro program bude mnohem obtížnější se jich zbavit. Na druhou stranu, pokud zakážete úpravu sdílených dat různými vlákny, vícevláknové programování .NET se prakticky nebude lišit od omezených funkcí VB6.

Nabízíme vám malý program, který demonstruje vzniklé problémy, aniž byste se pouštěli do zbytečných detailů. Tento program simuluje dům s termostatem v každé místnosti. Pokud je teplota o 5 nebo více stupňů Fahrenheita (asi 2,77 stupně Celsia) nižší než normálně, řekneme topnému systému, aby zvýšil teplotu o 5 stupňů; jinak teplota stoupne pouze o 1 stupeň. Pokud je aktuální teplota vyšší nebo rovna nastavené teplotě, neprovede se žádná změna. Regulace teploty v každé místnosti se provádí samostatným proudem se zpožděním 200 milisekund. Hlavní práci dělá následující úryvek:

Pokud mHouse.HouseTemp< mHouse.MAX_TEMP = 5 Then Try

Thread.Sleep(200)

Catch tie As ThreadInterruptedException

„Pasivní čekání bylo přerušeno

Catch e As Exception

" Ostatní výjimky Konec Pokus

mHouse.HouseTemp +- 5" atd.

Níže je kompletní zdrojový kód programu. Výsledek je znázorněn na Obr. 10.6: Teplota v domě dosáhla 105 stupňů Fahrenheita (40,5 stupně Celsia)!

1 Možnost Striktně Zapnuto

2 Importy System.Threading

3 Modul

4 SubMain()

5 Dim myHouse As New House(l0)

6 konzole. ReadLine()

7 End Sub

8 Koncový modul

9 Dům veřejné třídy

10 Public Const MAX_TEMP As Integer = 75

11 Private mCurTemp As Integer = 55

12 soukromých pokojů mRooms() Jako pokoj

13 Public Sub New (ByVal numOfRooms As Integer)

14 ReDim mRooms (numOfRooms = 1)

15 Dim i Jako celé číslo

16 Dim aThreadStart As Threading.ThreadStart

17 Dim aThread As Thread

18 Pro i = 0 až numOfRooms -1

19 Zkuste

20 mRooms(i)=NewRoom(Já, mCurTemp,CStr(i) &"throom")

21 aThreadStart - Nový ThreadStart (AddressOf _

mRooms(i).CheckTempInRoom)

22 aThread =Nové vlákno(aThreadStart)

23 aThread.Start()

24 Catch E jako výjimka

25 Console.WriteLine(E.StackTrace)

26 Ukončit pokus

27 Další

28 End Sub

29 Veřejný majetek HouseTemp() Jako celé číslo

třicet . Dostat

31 Vraťte mCurTemp

32 Konec Získat

33 Set (ByVal Value As Integer)

34 mCurTemp = Hodnota 35 End Set

36 Koncová vlastnost

37 Konec třídy

38 Veřejná třída

39 Private mCurTemp As Integer

40 Soukromé mName jako řetězec

41 Private mHouse As House

42 Public Sub New (ByVal the House As House,

ByVal temp As Integer, ByVal roomName As String)

43 mDům = Dům

44 mCurTemp = tepl

45 mName = název místnosti

46 End Sub

47 Public Sub CheckTempInRoom()

48 ChangeTemperature()

49 End Sub

50 Private Sub ChangeTemperature()

51 Zkuste

52 Pokud mHouse.HouseTemp< mHouse.MAX_TEMP - 5 Then

53 Thread.Sleep(200)

54 mHouse.HouseTemp +- 5

55 Console.WriteLine("Jsem v " & Me.mName & _

56 ". Aktuální teplota je "&mHouse.HouseTemp)

57. Elself mHouse.HouseTemp< mHouse.MAX_TEMP Then

58 Thread.Sleep(200)

59 mHouse.HouseTemp += 1

60 Console.WriteLine("Jsem v " & Me.mName & _

61 ". Aktuální teplota je " & mHouse.HouseTemp)

62 Jinak

63 Console.WriteLine("Jsem v " & Me.mName & _

64 ". Aktuální teplota je " & mHouse.HouseTemp)

65 „Nedělejte nic, teplota je normální

66 End If

67 Catch tae As ThreadInterruptedException

68 "Pasivní čekání bylo přerušeno

69 Catch e As Exception

70" Další výjimky

71 Ukončit pokus

72 End Sub

73 Konec třídy

Rýže. 10.6. Problémy s vícevlákny

V proceduře Sub Main (řádky 4-7) je vytvořen "dům" s deseti "místnostmi". Třída House nastavuje maximální teplotu na 75 stupňů Fahrenheita (asi 24 stupňů Celsia). Řádky 13-28 definují poměrně složitý stavitel domu. Řádky 18-27 jsou klíčové pro pochopení programu. Řádek 20 vytvoří další objekt místnosti a předá odkaz na objekt domu konstruktérovi, aby se na něj v případě potřeby mohl objekt místnosti odkazovat. Řádky 21-23 mají deset vláken pro úpravu teploty v každé místnosti. Třída Místnost je definována na řádcích 38-73. Odkaz na objekt House coxpaNastavte na proměnnou mHouse v konstruktoru třídy Místnost (řádek 43). Kód pro kontrolu a úpravu teploty (řádky 50-66) vypadá jednoduše a přirozeně, ale jak brzy uvidíte, tento dojem klame! Všimněte si, že tento kód je zabalen do bloku Try-Catch, protože program používá metodu spánku.

Je nepravděpodobné, že by někdo souhlasil s tím, že bude žít při teplotě 105 stupňů Fahrenheita (40,5 ± 24 stupňů Celsia). Co se stalo? Problém je s následujícím řádkem:

Pokud mHouse.HouseTemp< mHouse.MAX_TEMP - 5 Then

Stane se, že vlákno 1 nejprve zkontroluje teplotu. Vidí, že teplota je příliš nízká a zvýší ji o 5 stupňů. Bohužel před zvýšením teploty se vlákno 1 přeruší a řízení se přenese na vlákno 2. Vlákno 2 kontroluje stejnou proměnnou, která zatím nebyl změněn závit 1. Závit 2 se tedy také připravuje na zvýšení teploty o 5 stupňů, ale nestihne to udělat a také přejde do stavu čekání. Proces pokračuje, dokud se vlákno 1 neaktivuje a nepřejde k dalšímu příkazu - zvýšení teploty o 5 stupňů. Navýšení se opakuje při aktivaci všech 10 streamů a obyvatelé domu se budou mít špatně.

Řešení problémů: Synchronizace

V předchozím programu nastala situace, kdy výsledek programu závisí na pořadí, ve kterém jsou vlákna vykonávána. Abyste se toho zbavili, musíte se ujistit, že příkazy jako

Pokud mHouse.HouseTemp< mHouse.MAX_TEMP - 5 Then...

jsou plně zpracovány aktivním vláknem před jeho přerušením. Tato vlastnost se nazývá atomová hanba - blok kódu musí být proveden každým vláknem bez přerušení jako atomová jednotka. Skupinu instrukcí spojených v atomickém bloku nemůže plánovač vláken přerušit, dokud není dokončena. Každý vícevláknový programovací jazyk má své vlastní způsoby zajištění atomicity. Ve VB .NET je nejjednodušší použít příkaz SyncLock, který se volá s proměnnou objektu. Proveďte drobné změny v postupu ChangeTemperature z předchozího příkladu a program bude fungovat dobře:

Private Sub ChangeTemperature() SyncLock(mHouse)

Snaž se

Pokud mHouse.HouseTemp< mHouse.MAXJTEMP -5 Then

Thread.Sleep(200)

mHouse.HouseTemp += 5

Console.WriteLine("Jsem v " & Me.mName & _

".Aktuální teplota je " & mHouse.HouseTemp)

sami sebe

mHouse.HouseTemp< mHouse. MAX_TEMP Then

Thread.Sleep(200) mHouse.HouseTemp += 1

Console.WriteLine("Am in " & Me.mName &_ ".Aktuální teplota je " & mHouse.HomeTemp) Else

Console.WriteLineC"Am in " & Me.mName & _ ".Aktuální teplota je " & mHouse.HouseTemp)

„Nedělejte nic, teplota je normální

End If Catch tie As ThreadInterruptedException

"Pasivní čekání bylo přerušeno funkcí Catch e As Exception

„Další výjimky

Ukončit pokus

EndSyncLock

konec sub

Blokový kód SyncLock se provádí atomicky. Přístup k němu ze všech ostatních vláken bude uzavřen, dokud první vlákno neuvolní zámek pomocí příkazu End SyncLock. Pokud vlákno v synchronizovaném bloku přejde do stavu pasivního čekání, zámek je podržen, dokud vlákno není přerušeno nebo obnoveno.

Správné použití příkazu SyncLock zajišťuje, že váš program je bezpečný pro vlákna. Bohužel zneužívání SyncLock negativně ovlivňuje výkon. Synchronizace kódu ve vícevláknovém programu několikanásobně snižuje rychlost jeho práce. Synchronizujte pouze nejnutnější kód a co nejdříve odstraňte zámek.

Základní třídy kolekce nejsou bezpečné pro vlákna, ale rozhraní .NET Framework obsahuje verze většiny tříd kolekce s bezpečnými vlákny. V těchto třídách je kód pro potenciálně nebezpečné metody uzavřen v blocích SyncLock. Bezpečné verze tříd kolekce by měly být používány ve vícevláknových programech, kdykoli je narušena integrita dat.

Zbývá zmínit, že podmínkové proměnné lze snadno implementovat pomocí příkazu SyncLock. Chcete-li to provést, stačí synchronizovat zápis do běžné booleovské vlastnosti pro čtení a zápis, jak je to provedeno v následujícím úryvku:

Public Class ConditionVariable

Soukromá sdílená skříňka jako objekt = nový objekt ()

Private Shared mOK As Boolean Shared

Vlastnost TheConditionVariable()Jako Boolean

Dostat

Návrat OK

Konec dostat

Set (ByVal Value As Boolean) SyncLock (skříňka)

mOK= Hodnota

EndSyncLock

koncová sada

Koncová vlastnost

závěrečná třída

Příkaz SyncLock a třída Monitor

Použití příkazu SyncLock zahrnuje některé jemnosti, které nejsou uvedeny ve výše uvedených jednoduchých příkladech. Volba synchronizačního objektu tedy hraje velmi důležitou roli. Zkuste spustit předchozí program s SyncLock(Me) místo SyncLock(mHouse). Teplota opět stoupá nad prahovou hodnotu!

Pamatujte, že příkaz SyncLock se synchronizuje podle objekt, předán jako parametr, nikoli pomocí fragmentu kódu. Parametr SyncLock funguje jako dveře pro přístup k synchronizovanému fragmentu z jiných vláken. Příkaz SyncLock(Me) ve skutečnosti otevře několik různých dveří, což je přesně to, čemu jste se snažili předejít synchronizací. Morálka:

Chcete-li chránit sdílená data ve vícevláknové aplikaci, musí se příkaz SyncLock synchronizovat na jediném objektu.

Vzhledem k tomu, že synchronizace je objektově specifická, je v některých situacích možné neúmyslně zablokovat jiné fragmenty. Řekněme, že máte dvě synchronizované metody první a druhou, přičemž obě metody jsou synchronizovány na objektu bigLock. Když vlákno 1 vstoupí do první metody a chytne bigLock, žádné vlákno nebude moci vstoupit do druhé metody, protože přístup k ní je již omezen na vlákno 1!

Funkčnost příkazu SyncLock lze považovat za podmnožinu funkčnosti třídy Monitor. Třída Monitor je vysoce přizpůsobitelná a lze ji použít k řešení netriviálních úloh synchronizace. Příkaz SyncLock je přibližnou obdobou metod Enter a Exit třídy Monitor:

Snaž se

Monitor.Enter(theObject) Konečně

Monitor.Exit(objekt)

Ukončit pokus

Pro některé standardní operace (zvyšování/snižování proměnné, výměna obsahu dvou proměnných) poskytuje .NET Framework třídu Interlocked, jejíž metody provádějí tyto operace na atomické úrovni. Při použití třídy Interlocked jsou tyto operace mnohem rychlejší než použití příkazu SyncLock.

Zablokování

Během synchronizace je zámek nastaven na objekty, nikoli vlákna, takže při použití odlišný objekty k blokování odlišný fragmenty kódu v programech mají někdy velmi netriviální chyby. Bohužel v mnoha případech je jednoobjektová synchronizace prostě nepřijatelná, protože způsobí příliš časté blokování vláken.

Zvažte situaci blokování(deadlock) ve své nejjednodušší podobě. Představte si dva programátory u jídelního stolu. Bohužel pro dva z nich mají jen jeden nůž a jednu vidličku. Za předpokladu, že k jídlu potřebujete nůž i vidličku, jsou možné dvě situace:

  • Jednomu programátorovi se podaří uchopit nůž a vidličku a začne jíst. Když je nasycen, odloží jídelní soupravu a pak si je může vzít jiný programátor.
  • Jeden programátor vezme nůž a druhý vidličku. Ani jeden nebude moci začít jíst, pokud druhý nepředá svůj spotřebič.

Ve vícevláknovém programu se tato situace nazývá vzájemné blokování. Tyto dvě metody jsou synchronizovány na různých objektech. Vlákno A uchopí objekt 1 a vstoupí do fragmentu programu chráněného tímto objektem. Bohužel potřebuje přístup ke kódu chráněnému jiným blokem synchronizačního zámku s jiným synchronizačním objektem, aby fungoval. Než však může vstoupit do fragmentu synchronizovaného jiným objektem, vlákno B vstoupí a uchopí tento objekt. Nyní vlákno A nemůže vstoupit do druhého fragmentu, vlákno B nemůže vstoupit do prvního fragmentu a obě vlákna jsou odsouzena k nekonečnému čekání. Žádné vlákno nemůže pokračovat v běhu, protože objekt potřebný k tomu nebude nikdy uvolněn.

Diagnostika uváznutí je obtížná, protože k nim může dojít v relativně vzácných případech. Vše závisí na pořadí, ve kterém jim plánovač přiděluje CPU čas. Je zcela možné, že ve většině případů budou objekty synchronizace získány v pořadí, které není uvázlé.

Níže je uvedena implementace právě popsané zablokování. Po krátké diskuzi o nejzásadnějších bodech si ukážeme, jak rozpoznat zablokování v okně vlákna:

1 Možnost Striktně Zapnuto

2 Importy System.Threading

3 Modul

4 SubMain()

5 Dim Tom jako nový programátor ("Tom")

6 Dim Bob jako nový programátor ("Bob")

7 Dim aThreadStart As New ThreadStart (AddressOf Tom.Eat)

8 Dim aThread As New Thread (aThreadStart)

9 aThread.Name="Tom"

10 Dim bThreadStart As New ThreadStarttAddressOf Bob.Eat)

11 Dim bThread jako nové vlákno (bThreadStart)

12 bThread.Name = "Bob"

13 aThread.Start()

14 bThread.Start()

15 End Sub

16 Koncový modul

17 Vidlička veřejné třídy

18 Private Shared mForkAvaiTable As Boolean = True

19 Soukromý sdílený vlastník jako řetězec = "nikdo"

20 Soukromá vlastnost pouze pro čtení OwnsUtensil() jako řetězec

21 Získejte

22 Vraťte mOwnera

23 Konec Získat

24 Koncová vlastnost

25 Public Sub GrabForktByVal jako programátor)

26 Console.Writel_ine(Thread.CurrentThread.Name &_

"pokouším se chytit vidličku.")

27 Console.WriteLine(Me.OwnsUtensil & "má vidlici.") . .

28 Monitor.Zadejte(já) "SyncLock(aFork)"

29 Pokud je mForkAvailable Then

30 a.HasFork = Pravda

31 mOwner = a.MyName

32 mForkAvailable= Nepravdivé

33 Console.WriteLine(a.MojeJméno&"právě jsem dostal vidlici.čeká")

34 Zkuste

Thread.Sleep(100) Catch e As Exception Console.WriteLine(e.StackTrace)

Ukončit pokus

35 End If

36 Monitor.Konec (já)

EndSyncLock

37 End Sub

38 Konec třídy

39 Nůž veřejné třídy

40 Private Shared mKnifeAvailable As Boolean = True

41 Private Shared mOwner As String ="Nikdo"

42 Soukromá vlastnost pouze pro čtení OwnsUtensi1() jako řetězec

43 Získejte

44 Vraťte sekačku

45 Konec Získat

46 Koncová vlastnost

47 Public Sub GrabKnifetByVal jako programátor)

48 Console.WriteLine(Thread.CurrentThread.Name & _

"pokouším se chytit nůž.")

49 Console.WriteLine(Me.OwnsUtensil & "má nůž.")

50 Monitor.Zadejte(já) "SyncLock(aKnife)"

51 If mKnifeAvailable Then

52 mKnifeAvailable = False

53 a.HasKnife = Pravda

54 mOwner = a.MyName

55 Console.WriteLine(a.MyName&"právě mám nůž.čeká")

56 Zkuste

Thread.Sleep(100)

Catch e As Exception

Console.WriteLine(e.StackTrace)

Ukončit pokus

57 End If

58 Monitor.Konec (Já)

59 End Sub

60 konec třídy

61 Programátor veřejné třídy

62 Soukromé mName jako řetězec

63 Private Shared mFork As Fork

64 Private Shared mKnife As Knife

65 Private mHasKnife As Boolean

66 Private mHasFork jako booleovský

67 Shared Sub New()

68 mFork = nová vidlice()

69 mNůž = Nový nůž()

70 End Sub

71 Public Sub New (ByVal theName As String)

72 mName = theName

73 End Sub

74 Veřejná vlastnost pouze pro čtení MyName() As String

75 Získejte

76 Návrat mName

77 Konec Získat

78 Koncová vlastnost

79 Public Property HasKnife() As Boolean

80 Získejte

81 Vraťte mHasKnife

82 Konec Získat

83 Set (ByVal Value As Boolean)

84 mHasKnife = Hodnota

85 Konec setu

86 Koncová vlastnost

87 Public Property HasFork() Jako logická hodnota

88 Získejte

89 Vraťte mHasFork

90 Konec Získat

91 Set (ByVal Value As Boolean)

92 mHasFork = hodnota

93 Konec sady

94 Koncová vlastnost

95 Public Sub Eat()

96 Dělej dokud já.HasKnife And Me.HasFork

97 Console.Writeline(Thread.CurrentThread.Name&"je ve vláknu.")

98 If Rnd()< 0.5 Then

99 mFork.GrabFork(Me)

100 dalších

101 mKnife.GrabKnife(Me)

102 End If

103 Smyčka

104 MsgBox(Já.MojeJméno & "můžem jíst!")

105 mNůž = Nový nůž()

106 mFork= Nová vidlice()

107 End Sub

108 Konec třídy

Hlavní procedura Main (řádky 4-16) vytvoří dvě instance třídy Programmer a poté spustí dvě vlákna pro provedení kritické metody Eat třídy Programmer (řádky 95-108), popsané níže. Hlavní procedura nastavuje názvy vláken a zavádí je; pravděpodobně je vše, co se děje, jasné a bez komentáře.

Zajímavěji vypadá kód třídy Fork (řádky 17-38) (podobná třída Knife je definována na řádcích 39-60). Řádky 18 a 19 nastavují hodnoty společných polí, pomocí kterých můžete zjistit, zda je vidlice aktuálně dostupná, a pokud ne, kdo ji používá. Vlastnost ReadOnly OwnUtensi1 (řádky 20-24) je určena pro nejjednodušší přenos informací. Ústředním prvkem třídy Fork je metoda GrabFork „uchopte vidlici“, definovaná na řádcích 25-27.

  1. Řádky 26 a 27 jednoduše zapisují informace o ladění do konzole. V hlavním kódu metody (řádky 28-36) je přístup k vidlici synchronizován na cestě objektu.pás Me. Protože náš program používá pouze jednu větev, načasování na Me zajišťuje, že jej nemohou zachytit dvě vlákna současně. Příkaz Sleep "p (v bloku začínajícím na řádku 34) simuluje prodlevu mezi uchopením vidličky/nože a začátkem jídla. Všimněte si, že příkaz Sleep neuvolní zámek na předmětech a pouze urychlí zablokování!
    Nejzajímavější je však kód třídy Programmer (řádky 61-108). Řádky 67-70 definují obecný konstruktor, který zajistí, že v programu bude pouze jedna vidlička a nůž. Kód vlastnosti (řádky 74-94) je jednoduchý a nevyžaduje komentáře. To nejdůležitější se děje v metodě Eat, která je prováděna dvěma samostatnými vlákny. Proces pokračuje ve smyčce, dokud nějaké vlákno nezachytí vidličku spolu s nožem. Na řádcích 98-102 objekt náhodně uchopí vidličku/nůž pomocí volání Rnd, což způsobuje uváznutí. Nastane následující:
    Vlákno provádějící metodu Eat objektu One se aktivuje a vstoupí do smyčky. Popadne nůž a přejde do stavu čekání.
  2. Vlákno provádějící Bobovu metodu Eat se probudí a vstoupí do smyčky. Nemůže uchopit nůž, ale uchopí vidličku a přejde do stavu čekání.
  3. Vlákno provádějící metodu Eat objektu One se aktivuje a vstoupí do smyčky. Snaží se chytit vidličku, ale vidličku už chytil Bob; vlákno přejde do stavu čekání.
  4. Vlákno provádějící Bobovu metodu Eat se probudí a vstoupí do smyčky. Pokusí se chytit nůž, ale nůž už popadl Thoth; vlákno přejde do stavu čekání.

To vše pokračuje do nekonečna - máme typickou patovou situaci (zkuste spustit program a uvidíte, že nikdo nedokáže tak jíst).
O výskytu uváznutí se také můžete dozvědět v okně vláken. Spusťte program a přerušte jej klávesami Ctrl+Break. Zahrňte do výřezu proměnnou Me a otevřete okno vláken. Výsledek vypadá podobně jako na obr. 10.7. Z obrázku je vidět, že Bobova nit popadla nůž, ale nemá vidličku. Klepněte pravým tlačítkem myši v okně vláken na řádek Toth a z místní nabídky vyberte příkaz Přepnout na vlákno. Výřez ukazuje, že potok Thoth má vidličku, ale žádný nůž. Není to samozřejmě stoprocentní důkaz, ale takové chování člověka přinejmenším vzbuzuje podezření, že něco nebylo v pořádku.
Pokud možnost synchronizace na jediném objektu (jako v programu se zvýšením teploty v domě) není možná, můžete synchronizační objekty očíslovat a zachytit je vždy v konstantním pořadí, abyste předešli uváznutí. Abychom pokračovali v analogii s programátory na večeři: pokud vlákno vždy nejprve vezme nůž a poté vidličku, nenastanou žádné problémy se zablokováním. První vlákno, které chytne nůž, bude moci normálně jíst. Přeloženo do jazyka programových toků to znamená, že zachycení objektu 2 je možné pouze tehdy, je-li objekt 1 dříve zachycen.

Rýže. 10.7. Analýza uváznutí v okně Vlákna

Pokud tedy odstraníme volání Rnd na lince 98 a nahradíme jej fragmentem

mFork.GrabFork(Me)

mKnife.GrabKnife(Já)

mrtvý bod zmizí!

Spolupracujte na datech při jejich vytváření

Běžnou situací ve vícevláknových aplikacích je situace, kdy vlákna nejen pracují se sdílenými daty, ale také čekají, až dorazí (to znamená, že vlákno 1 musí data vytvořit, než je může použít vlákno 2). Protože jsou data sdílena, přístup k nim musí být synchronizován. Je také nutné poskytnout prostředky pro upozornění čekajících vláken, když jsou data připravena.

Takové situaci se většinou říká problém dodavatele/spotřebitele. Vlákno se pokouší získat přístup k datům, která ještě neexistují, takže musí přenést řízení na jiné vlákno, které vytvoří požadovaná data. Problém je vyřešen pomocí následujícího kódu:

  • Vlákno 1 (spotřebitel) se probudí, vstoupí do synchronizované metody, hledá data, nenajde je a přejde do stavu čekání. PředběžnýMusí však uvolnit zámek, aby nenarušoval práci vlákna poskytovatele.
  • Vlákno 2 (poskytovatel) vstoupí do synchronizované metody uvolněné vláknem 1, vytváří data pro vlákno 1 a nějak upozorní vlákno 1 na přítomnost dat. Poté uvolní zámek, aby vlákno 1 mohlo zpracovat nová data.

Nepokoušejte se tento problém vyřešit neustálou aktivací vlákna 1 při kontrole stavu proměnné podmínky, jejíž hodnota je >nastavena vláknem 2. Toto řešení vážně ovlivní výkon vašeho programu, protože ve většině případů se vlákno 1 probudí bez důvodu; a vlákno 2 bude čekat tak často, že nestihne vytvořit data.

Vztahy producent/spotřebitel jsou velmi běžné, takže knihovny tříd programování s více vlákny vytvářejí pro takové situace speciální primitiva. V .NET se tato primitiva nazývají Wait a Pulse-PulseAl 1 a jsou součástí třídy Monitor. Obrázek 10.8 vysvětluje situaci, kterou se chystáme naprogramovat. V programu jsou tři fronty vláken: fronta čekání, fronta blokování a fronta provádění. Plánovač podprocesů nepřiděluje čas procesoru podprocesům, které jsou ve frontě čekání. Aby byl vláknu přidělen čas, musí se přesunout do spouštěcí fronty. Výsledkem je, že práce aplikace je organizována mnohem efektivněji než u obvyklého dotazování podmíněné proměnné.

V pseudokódu je idiom spotřebitele dat formulován následovně:

" Zadání synchronizovaného bloku následujícího formuláře

zatímco žádná data

Přejít do čekající fronty

smyčka

Pokud existují data, zpracujte je.

Opustit synchronizovaný blok

Ihned po provedení příkazu Wait je vlákno pozastaveno, zámek je uvolněn a vlákno vstoupí do čekací fronty. Když je zámek uvolněn, vlákno ve frontě běhu může běžet. V průběhu času jedno nebo více blokovaných vláken vytvoří data potřebná pro práci vlákna ve frontě čekání. Vzhledem k tomu, že ověřování dat se provádí ve smyčce, přechod na používání dat (po smyčce) nastává pouze tehdy, když jsou data připravena ke zpracování.

V pseudokódu vypadá idiom poskytovatele dat takto:

"Zadání bloku synchronizovaného zobrazení

Zatímco data NENÍ potřeba

Přejít do čekající fronty

Jinak Vytvářejte data

Až budou data připravena, zavolejte Pulse-PulseAll.

přesunout jedno nebo více vláken z blokovací fronty do spouštěcí fronty. Opustit synchronizovaný blok (a vrátit se do fronty běhu)

Předpokládejme, že náš program simuluje rodinu s jedním rodičem, který vydělává peníze, a dítětem, které tyto peníze utrácí. Když peníze skončíutsya, dítě musí čekat na příchod nové částky. Softwarová implementace tohoto modelu vypadá takto:

1 Možnost Striktně Zapnuto

2 Importy System.Threading

3 Modul

4 SubMain()

5 Dim theFamily jako nová rodina()

6 theFamily.StartltsLife()

7 End Sub

8 Konec fjodule

9

10 Rodina veřejné třídy

11 Soukromé mPeníze jako celé číslo

12 Private mWeek As Integer = 1

13 Public Sub StartltsLife()

14 Dim aThreadStart jako nový ThreadStarUAddressOf Me.Produce)

15 Dim bThreadStart jako nový ThreadStarUAddressOf Me.Consume)

16 Dim aThread As New Thread (aThreadStart)

17 Ztlumit bThread jako nové vlákno (bThreadStart)

18 aThread.Name = "Produkovat"

19 aThread.Start()

20 bThread.Name = "Spotřebujte"

21 bVlákno. Start()

22 End Sub

23 Public Property TheWeek() Jako celé číslo

24 Získejte

25 Návrat mtýd

26 Konec Získat

27 Set (ByVal Value As Integer)

28 mtýden - Hodnota

29 Konec sady

30 Koncová vlastnost

31 Veřejný majetek OurMoney() Jako celé číslo

32 Získejte

33 Vraťte mPeníze

34 Konec Získat

35 Set (ByVal Value As Integer)

36 milionů peněz = hodnota

37 Konec sady

38 Koncová vlastnost

39 Public SubProduce()

40 Thread.Sleep(500)

41 Udělejte

42 Monitor.Zadejte(já)

43 Dělej, zatímco já. Naše peníze > 0

44 Monitor. Počkejte (já)

45 Smyčka

46 Já.NašePeníze=1000

47 Monitor.PulseAll(Me)

48 Monitor.Konec (Já)

49 Smyčka

50 End Sub

51 Public Sub Consume()

52 MsgBox("Jsem ve vláknu spotřeby")

53 Udělejte

54 Monitor.Zadejte(já)

55 Dělej, zatímco já. Naše peníze = 0

56 Monitor. Počkejte (já)

57 Smyčka

58 Console.WriteLine("Drahý rodiči, právě jsem utratil všechny tvé" & _

peníze za týden" & The Week)

59 Týden += 1

60 If TheWeek = 21 *52 Then System.Environment.Exit(0)

61 Já.Naše peníze=0

62 Monitor.PulseAll(Me)

63 Monitor.Exit(Me)

64 Smyčka

65 End Sub

66 Konec třídy

Metoda StartltsLife (řádky 13-22) připravuje spuštění vláken Produce a Consume. To nejdůležitější se děje v tocích Produce (řádky 39-50) a Consume (řádky 51-65). Procedura Sub Produce zkontroluje dostupnost peněz, a pokud jsou peníze, přejdou do fronty. V opačném případě rodič vygeneruje peníze (řádek 46) a upozorní objekty ve frontě na změnu situace. Pamatujte, že volání Pulse-Pulse All se projeví pouze tehdy, když je zámek uvolněn příkazem Monitor.Exit. Naopak procedura Sub Consume zkontroluje přítomnost peněz, a pokud žádné peníze nejsou, upozorní nastávajícího rodiče. Linka 60 jednoduše ukončí program po 21 podmíněných letech; zavolejte Systém. Environment.Exit(0) je .NET protějšek příkazu End (příkaz End je také podporován, ale na rozdíl od System.Environment.Exit nevrací operačnímu systému kód ukončení).

Vlákna umístěná ve frontě čekání musí být uvolněna jinými částmi vašeho programu. To je důvod, proč raději používáme PulseAll místo Pulse. Protože není předem známo, které vlákno bude aktivováno při volání Pulse 1, s relativně malým počtem vláken ve frontě lze PulseAll volat se stejným úspěchem.

Multithreading v grafických programech

Naše diskuse o multithreadingu v GUI aplikacích začne příkladem vysvětlujícím, k čemu je multithreading v GUI aplikacích. Vytvořte formulář se dvěma tlačítky Start (btnStart) a Cancel (btnCancel), jak je znázorněno na obrázku 1. 10.9. Po kliknutí na tlačítko Start se vytvoří třída, která obsahuje náhodný řetězec 10 milionů znaků a metodu pro počítání výskytů písmene "E" v tomto dlouhém řetězci. Všimněte si použití třídy StringBuilder, díky které je vytváření dlouhých řetězců efektivnější.

Krok 1

Vlákno 1 si všimne, že pro něj nejsou žádná data. Zavolá Wait, uvolní zámek a zařadí se do čekací fronty.



Krok 2

Když je zámek uvolněn, vlákno 2 nebo vlákno 3 opustí frontu zámků a vstoupí do synchronizovaného bloku, čímž získá zámek

Krok 3

Řekněme, že vlákno 3 vstoupí do synchronizovaného bloku, vytvoří data a zavolá Pulse-Pulse All.

Ihned poté, co opustí blok a uvolní zámek, se vlákno 1 přesune do fronty provádění. Pokud vlákno 3 zavolá Pluse, pouze jedno přejde do fronty běhu.vlákno, když se zavolá Pluse All, všechna vlákna přejdou do fronty běhu.



Rýže. 10.8. Problém dodavatele/spotřebitele

Rýže. 10.9. Multithreading v jednoduché GUI aplikaci

Importuje System.Text

Náhodné znaky veřejné třídy

Private m_Data As StringBuilder

Private mjength, m_count As Integer

Public Sub New (ByVal n As Integer)

m_Length = n-1

m_Data = New StringBuilder(m_length) MakeString()

konec sub

Private Sub MakeString()

Dim i As Integer

Dim myRnd As New Random()

Pro i = 0 až m_délka

"Vygenerujte náhodné číslo od 65 do 90,

" převést na velká písmena

" a připojte se k objektu StringBuilder

m_Data.Append(Chr(myRnd.Next(65,90)))

další

konec sub

Public Sub StartCount()

GetEes()

konec sub

Private Sub GetEes()

Dim i As Integer

Pro i = 0 až m_délka

If m_Data.Chars(i) = CChar("E") Then

m_count += 1

End If Next

m_CountDone = Pravda

konec sub

Veřejné pouze pro čtení

Vlastnost GetCount() As Integer Get

Pokud ne (m_CountDone) Potom

Vraťte m_count

End If

End Get End Property

Veřejné pouze pro čtení

Vlastnost IsDone() As Boolean Get

vrátit se

m_CountDone

Konec dostat

Koncová vlastnost

závěrečná třída

Dvě tlačítka ve formuláři mají velmi jednoduchý kód spojený s nimi. Procedura btn-Start_Click vytvoří instanci výše uvedené třídy RandomCharacters, která zapouzdří řetězec s 10 miliony znaků:

Private Sub btnStart_Click(ByVal odesílatel As System.Object.

ByVal e As System.EventArgs) Zvládá btnSTART.Click

Dim RC jako nové náhodné znaky (1 000 000)

RC.StartCount()

MsgBox("Počet es je " & RC.GetCount)

konec sub

Tlačítko Storno zobrazí okno se zprávou:

Private Sub btnCancel_Click(ByVal sender As System.Object._

ByVal e As System.EventArgs) Zvládá btnCancel.Click

MsgBox("Počítání přerušeno!")

konec sub

Když spustíte program a kliknete na tlačítko Start, zjistíte, že tlačítko Storno nereaguje na vstup uživatele, protože nepřetržitá smyčka brání tlačítku zpracovat přijatou událost. V moderních programech je to nepřijatelné!

Jsou možná dvě řešení. První možnost, dobře známá z předchozích verzí VB, se obejde bez multithreadingu: volání DoEvents je zahrnuto do smyčky. V .NET vypadá tento příkaz takto:

Application.DoEvents()

V našem příkladu je to rozhodně nežádoucí – kdo chce zpomalit program deseti miliony volání DoEvents! Pokud místo toho rozdělíte smyčku do samostatného vlákna, operační systém se mezi vlákny přepne a tlačítko Storno bude stále fungovat. Implementace se samostatným vláknem je zobrazena níže. Abychom vizuálně ukázali, že tlačítko Storno funguje, po jeho stisknutí jednoduše ukončíme program.

Další krok: Zobrazit tlačítko počítání

Řekněme, že se rozhodnete být kreativní a dát formuláři vzhled znázorněný na obr. 10.9. Upozornění: tlačítko Zobrazit počet zatím není dostupné.

Rýže. 10.10. Formulář s deaktivovaným tlačítkem

Samostatné vlákno má provést počítání a odemknout deaktivované tlačítko. Samozřejmě to lze udělat; navíc takový problém nastává poměrně často. Bohužel to nebudete moci udělat tím nejviditelnějším způsobem – propojením sekundárního vlákna s vláknem GUI ponecháním odkazu na tlačítko ShowCount v konstruktoru, nebo dokonce pomocí standardního delegáta. Jinými slovy, nikdy nepoužívejte níže uvedenou možnost (zákl chybnýřádky jsou tučně).

Náhodné znaky veřejné třídy

Private m_0ata Jako StringBuilder

Private m_CountDone As Boolean

Soukromá mjength. m_count As Integer

Private m_Button Jako Windows.Forms.Button

Public Sub New(ByVa1 n As Integer,_

ByVal b AsWindows.Forms.Button)

m_délka = n - 1

m_Data = New StringBuilder (mJength)

m_Button = b MakeString()

konec sub

Private Sub MakeString()

Dim I As Integer

Dim myRnd As New Random()

Pro I = 0 až m_délka

m_Data.Append(Chr(myRnd.Next(65,90)))

další

konec sub

Public Sub StartCount()

GetEes()

konec sub

Private Sub GetEes()

Dim I As Integer

Pro I = 0 až mjength

If m_Data.Chars(I) = CChar("E") Then

m_count += 1

End If Next

m_CountDone =Pravda

m_Button.Enabled=Pravda

konec sub

Veřejné pouze pro čtení

Vlastnost GetCount() As Integer

Dostat

Pokud ne (m_CountDone) Potom

Vyhoďte novou výjimku ("Počet ještě není dokončen") Jinak

Vraťte m_count

End If

Konec dostat

Koncová vlastnost

Veřejná vlastnost pouze pro čtení IsDone() jako logická hodnota

Dostat

Vraťte m_CountDone

Konec dostat

Koncová vlastnost

závěrečná třída

Je pravděpodobné, že v některých případech bude tento kód fungovat. Nicméně:

  • Interakce sekundárního vlákna s vláknem, které vytváří GUI, nelze zařídit zřejmé prostředek.
  • Nikdy neměňte prvky v grafických programech z jiných programových vláken. Ke všem změnám by mělo dojít pouze ve vláknu, které vytvořilo GUI.

Pokud tato pravidla porušíte, my garantujemeže se ve vašich vícevláknových grafických programech vyskytnou jemné, jemné chyby.

Interakce objektů nebude možné organizovat ani pomocí událostí. Pracovník událostí 06 běží na stejném vláknu, ve kterém byl volán RaiseEvent, takže vám události nepomohou.

Přesto zdravý rozum velí, že grafické aplikace by měly mít prostředky k úpravě prvků z jiného vlákna. Rozhraní .NET Framework poskytuje bezpečný způsob volání aplikačních metod GUI z jiného vlákna. K tomuto účelu se používá speciální typ delegáta Method Invoker ze jmenného prostoru System.Windows. formuláře. Následující úryvek ukazuje novou verzi metody GetEes (změněné řádky jsou tučně):

Private Sub GetEes()

Dim I As Integer

Pro I = 0 až m_délka

Pokud m_Data.Chars(I) = CChar("E")Potom

m_count += 1

End If Next

m_CountDone = Opravdový pokus

Dim myInvoker jako nový MethodInvoker (AddressOf UpDateButton)

myInvoker.Invoke() Catch e As ThreadInterruptedException

"Selhání

Ukončit pokus

konec sub

Public Sub UpdateButton()

m_Button.Enabled = Pravda

konec sub

Mezivláknová volání tlačítka se neprovádějí přímo, ale prostřednictvím Method Invoker. .NET Framework zaručuje, že tato možnost je bezpečná pro vlákna.

Proč vícevláknové programování způsobuje tolik problémů?

Nyní, když máte nějakou představu o vícevláknovém programování a potenciálních problémech s ním spojených, mysleli jsme si, že by bylo vhodné odpovědět na otázku položenou v záhlaví pododdílu na konci této kapitoly.

Jedním z důvodů je, že multithreading je nelineární proces a my jsme zvyklí na model lineárního programování. Zpočátku je těžké si zvyknout na samotnou myšlenku, že provádění programu může být náhodně přerušeno a řízení bude převedeno na jiný kód.

Existuje však ještě jeden, zásadnější důvod: příliš málo programátorů v dnešní době programuje v assembleru, nebo se dokonce dívá na rozebraný výstup kompilátoru. Jinak by si mnohem snáze zvykli na myšlenku, že desítky montážních návodů mohou odpovídat jednomu příkazu vysokoúrovňového jazyka (např. VB .NET). Vlákno může být přerušeno po kterémkoli z těchto pokynů, a tedy i uprostřed instrukce na vysoké úrovni.

Ale to není vše: moderní kompilátory optimalizují rychlost programů a počítačový hardware může zasahovat do správy paměti. V důsledku toho může kompilátor nebo hardware bez vašeho vědomí změnit pořadí příkazů uvedených ve zdrojovém kódu programu [ Mnoho kompilátorů optimalizuje operace kopírování kruhového pole jako pro i=0 až n:b(i)=a(i):ncxt. Kompilátor (nebo dokonce specializovaný správce paměti) může jednoduše vytvořit pole a poté jej naplnit jedinou operací kopírování namísto vícenásobného kopírování jednotlivých prvků!].

Doufáme, že vám tato vysvětlení pomohou lépe porozumět tomu, proč vícevláknové programování způsobuje tolik problémů – nebo budete alespoň méně překvapeni podivným chováním vašich vícevláknových programů!