K jakým účelům se používají vícevláknové systémy. Osm jednoduchých pravidel pro vývoj vícevláknových aplikací

Jaké téma vyvolává nejvíce otázek a potíží pro začátečníky? Když jsem se na to zeptal svého učitele a programátora Javy Alexandra Pryakhina, okamžitě odpověděl: „Multithreading“. Díky mu za nápad a pomoc při přípravě tohoto článku!

Podíváme se do vnitřního světa aplikace a jejích procesů, zjistíme, co je podstatou multithreadingu, kdy je to užitečné a jak to implementovat - příkladem je Java. Pokud se učíte jiný jazyk OOP, nebojte se: základní principy jsou stejné.

O streamech a jejich původu

Abychom porozuměli multithreadingu, nejprve pochopme, co je to proces. Proces je kus virtuální paměti a prostředků, které operační systém přiděluje ke spuštění programu. Pokud otevřete několik instancí stejné aplikace, systém přidělí proces pro každou z nich. 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 „Sledování systému“) a víte, že zbytečné spuštěné procesy zatěžují systém a ty „nejtěžší“ z nich často zamrznou, takže je třeba je násilně ukončit .

Uživatelé ale milují multitasking: nekrmte je chlebem - nechte je otevřít tucet oken a skákat tam a zpět. Nastává dilema: musíte zajistit současný provoz aplikací a zároveň snížit zátěž systému, aby nezpomaloval. Řekněme, že hardware nemůže držet krok s potřebami vlastníků - problém musíte vyřešit na softwarové úrovni.

Chceme, aby procesor za jednotku času vykonal více pokynů a zpracoval více dat. To znamená, že do každého časového segmentu musíme vložit více spuštěného kódu. Myslete na jednotku spuštění kódu jako na objekt - to je vlákno.

Složitý případ je snáze přístupný, pokud jej rozdělíte na několik jednoduchých. Je tomu tak při práci s pamětí: „Těžký“ proces je rozdělen na vlákna, která zabírají méně zdrojů a je větší pravděpodobnost, že dodají kód do kalkulačky (jak přesně - viz 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í nové.

Rozdíl mezi vlákny a procesy

    Vlákna používají paměť přidělenou pro proces a procesy vyžadují vlastní paměťový prostor. Vlákna se proto vytvářejí a dokončují rychleji: systém jim nemusí pokaždé přidělovat nový adresní prostor a poté jej uvolnit.

    Každý z procesů pracuje se svými vlastními daty - mohou si něco vyměňovat pouze prostřednictvím mechanismu meziprocesové komunikace. Vlákna mají přímý přístup k datům a zdrojům druhého: to, co se změnilo, je okamžitě k dispozici všem. Vlákno může v procesu ovládat „kolegy“, zatímco proces ovládá výhradně jeho „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 kousky, které lze zpracovat samostatnými vlákny, a poté výsledek spojte dohromady. Je to lepší než plodit procesy náročné na zdroje.

Proč se ale populární aplikace jako Firefox vydává cestou vytváření více procesů? Protože je to pro prohlížeč, že práce s izolovanými kartami je spolehlivá a flexibilní. Pokud je s jedním procesem něco v nepořádku, není nutné ukončovat celý program - je možné uložit alespoň část dat.

Co je multithreading

Dostáváme se tedy k hlavnímu bodu. Multithreading je, když je proces aplikace rozdělen na vlákna, která jsou zpracovávána paralelně - v jednu jednotku času - procesorem.

Výpočetní zátěž je rozložena mezi dvě nebo více jader, takže rozhraní a další součásti programu navzájem nezpomalují práci.

Vícevláknové aplikace lze spouštět také na jednojádrových procesorech, ale poté se postupně spustí vlákna: první fungovalo, jeho stav byl uložen - druhé bylo povoleno pracovat, uloženo - vráceno na první nebo spuštěno 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.

Počkejte 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 nedocházelo k záměně při práci se sdílenými prostředky, musí vlákna koordinovat své akce. K tomu si vyměňují informace pomocí signálů. Každé vlákno říká ostatním, co dělá a jaké změny očekávat. Data všech vláken o aktuálním stavu prostředků jsou tedy synchronizována.

Základní synchronizační nástroje

Vzájemné vyloučení (vzájemné vyloučení, zkráceně - mutex) - „příznak“ přecházející do vlákna, kterému je aktuálně povoleno pracovat se sdílenými prostředky. Eliminuje přístup jiných vláken do obsazené oblasti paměti. V aplikaci může být několik mutexů, které lze sdílet mezi procesy. Má to háček: mutex nutí aplikaci přistupovat pokaždé k jádru operačního systému, což je nákladné.

Semafor - umožňuje omezit počet vláken, která mají v danou chvíli přístup ke zdroji. Tím se sníží zatížení procesoru při provádění kódu, 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, při jejímž výskytu se ovládací prvek přenese do požadovaného vlákna. Toky si vyměňují data událostí, aby se vyvíjely a logicky pokračovaly ve svých akcích. Jeden přijal data, druhý zkontroloval jejich správnost, třetí je uložil na pevný disk. Události se liší ve způsobu jejich zrušení. Pokud potřebujete o události upozornit několik vláken, budete muset ručně nastavit funkci zrušení, aby se signál zastavil. Pokud existuje pouze jedno cílové vlákno, můžete vytvořit událost automatického resetování. Po dosažení proudu zastaví samotný signál. Události lze zařadit do fronty pro flexibilní řízení toku.

Kritická část - složitější mechanismus, který kombinuje čítač smyčky a semafor. Počitadlo vám umožňuje odložit začátek semaforu na požadovaný čas. Výhodou je, že jádro se aktivuje pouze v případě, že je sekce zaneprázdněna a je třeba zapnout semafor. Po 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ření nového vlákna k provedení úkolu znamená vytvoření instance třídy Thread a přidružení k požadovanému kódu. To lze provést dvěma způsoby:

    podtřída Nit;

    implementujte ve své třídě rozhraní Runnable a poté předejte instance třídy konstruktoru Thread.

Zatímco se nebudeme dotýkat tématu zablokování, když si vlákna navzájem blokují práci a zamrzají, to si necháme na další článek.

Příklad vícevláknové Javy: ping-pong s mutexy

Pokud si myslíte, že se stane něco strašného, ​​vydechněte. Uvažujeme o práci se synchronizačními objekty téměř hravou formou: dvě vlákna budou vyvolána mutexem. Ve skutečnosti však uvidíte skutečnou aplikaci, kde pouze jedno vlákno může zpracovávat veřejně dostupná data najednou.

Nejprve vytvoříme třídu, která dědí vlastnosti vlákna, které již známe, a napíšeme metodu kickBall:

Veřejná třída PingPongThread rozšiřuje Thread (PingPongThread (String name) (this.setName (name); // override the thread name) @Override public void run () (Ball ball = Ball.getBall (); while (ball.isInGame () ) (kickBall (ball);)) private neplatný kickBall (Ball ball) (if (! ball.getSide (). equals (getName ())) (ball.kick (getName ());))))

Nyní se postarejme o míč. Nebude s námi jednoduchý, ale zapamatovatelný: aby mohl říci, kdo ho zasáhl, z jaké strany a kolikrát. K tomu použijeme mutex: bude shromažďovat informace o práci každého z vláken - to umožní izolovaným vláknům komunikovat mezi sebou navzájem. Po 15. úderu vyneseme míč ze hry, abychom jej vážně nezranili.

Veřejná třída Ball (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) (kopy ++; strana = jméno hráče; System.out.println (kopy + "" + strana);) Řetězec getSide () (návratová strana;) boolean isInGame () (návrat (kopy< 15); } }

A nyní na scénu vstupují dvě vlákna hráčů. Říkejme jim bez dalších okolků Ping a Pong:

Veřejná třída PingPongGame (PingPongThread player1 = new PingPongThread ("Ping"); PingPongThread player2 = new PingPongThread ("Pong"); Ball ball; PingPongGame () (ball = Ball.getBall ();) neplatné startGame () hodí InterruptedEx .start (); player2.start ();))

"Plný stadion lidí - čas začít zápas." Oficiálně oznámíme zahájení schůzky - v hlavní třídě aplikace:

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

Jak vidíte, nic zuřivého zde není. Toto je zatím jen úvod do multithreadingu, ale vy už víte, jak to funguje, a můžete experimentovat - omezit trvání hry nikoli počtem úderů, ale například časem. K tématu multithreadingu se vrátíme později - podíváme se na balíček java.util.concurrent, knihovnu Akka a volatilní mechanismus. Pojďme si také promluvit o implementaci multithreadingu v Pythonu.

Vícevláknové programování se nijak zásadně neliší od psaní grafických uživatelských rozhraní řízených událostmi nebo dokonce od psaní jednoduchých sekvenčních aplikací. Zde platí všechna důležitá pravidla upravující zapouzdření, oddělení obav, volné spojování atd. Ale pro mnoho vývojářů je obtížné psát vícevláknové programy právě proto, že tato pravidla zanedbávají. Místo toho se pokoušejí uvést do praxe mnohem méně důležité znalosti o vláknech a synchronizačních primitivách, shromážděné z textů o vícevláknovém programování pro začátečníky.

Jaká jsou tedy tato pravidla

Další programátor, který stojí před problémem, si myslí: „Ach, přesně, musíme použít regulární výrazy.“ A teď už má dva problémy - Jamie Zawinski.

Další programátor, potýkající se s problémem, si myslí: „Ach jo, tady použiji streamy.“ A teď má deset problémů - Bill Schindler.

Příliš mnoho programátorů, kteří se zavazují psát vícevláknový kód, se dostane do pasti, jako hrdina Goethovy balady “ Čarodějův učeň“. Programátor se naučí, jak vytvořit spoustu vláken, která v zásadě fungují, ale dříve nebo později se vymknou kontrole a programátor neví, co má dělat.

Ale na rozdíl od kouzelnického výpadku nemůže nešťastný programátor doufat v příchod mocného čaroděje, který mávne hůlkou a obnoví pořádek. Místo toho programátor podniká ty nejhezčí triky a snaží se vyrovnat s neustále se objevujícími problémy. Výsledek je vždy stejný: získá se příliš komplikovaná, omezená, křehká a nespolehlivá aplikace. Má trvalou hrozbu zablokování a další nebezpečí spojená se špatným vícevláknovým kódem. Nemluvím ani o nevysvětlených pádech, špatném výkonu, neúplných nebo nesprávných pracovních výsledcích.

Možná vás napadlo: proč se to děje? Běžná mylná představa je: „Vícevláknové programování je velmi obtížné.“ Ale není tomu tak. Pokud je vícevláknový program nespolehlivý, pak obvykle selže ze stejných důvodů jako nekvalitní jednovláknový program. Programátor prostě nedodržuje základní, známé a osvědčené metody vývoje. Vícevláknové programy se zdají být pouze složitější, protože čím více paralelních vláken se pokazí, tím větší nepořádek způsobí - a mnohem rychleji, než by to udělalo jedno vlákno.

Mylná představa o „složitosti vícevláknového programování“ se rozšířila díky těm vývojářům, kteří se profesionálně vyvinuli v psaní jednovláknového kódu, poprvé se setkali s multithreadingem a nevyrovnávali se s tím. Ale místo toho, aby přehodnotili své předpojatosti a pracovní návyky, tvrdošíjně opravují skutečnost, že nechtějí nijak pracovat. Tito lidé se vymlouvají na nespolehlivý software a zmeškané termíny a opakují totéž: „vícevláknové programování je velmi obtížné“.

Vezměte prosím na vědomí, že výše mluvím o typických programech, které používají vícevláknové zpracování. Skutečně existují složité vícevláknové scénáře-stejně jako složité jednovláknové scénáře. Nejsou ale běžné. V praxi se od programátora zpravidla nevyžaduje nic nadpřirozeného. Data přesouváme, transformujeme, čas od času provedeme nějaké výpočty a nakonec informace uložíme do databáze nebo je zobrazíme na obrazovce.

Na vylepšení průměrného jednovláknového programu a jeho přeměně na vícevláknový není nic obtížného. Alespoň by nemělo. Obtíže vznikají ze dvou důvodů:

  • programátoři nevědí, jak aplikovat jednoduché, dobře osvědčené metody vývoje;
  • většina informací uvedených v knihách o vícevláknovém programování je technicky správná, ale zcela nepoužitelná pro řešení aplikovaných problémů.

Nejdůležitější koncepce programování jsou univerzální. Jsou stejně použitelné pro jednovláknové i vícevláknové programy. Programátoři topící se ve víru proudů se zkrátka nenaučili důležité lekce, když zvládli jednovláknový kód. Mohu to říci, protože tito vývojáři dělají stejné zásadní chyby v programech s více vlákny a s jedním vláknem.

Možná nejdůležitější lekce, kterou je třeba se naučit za šedesát let historie programování, je: globální mutovatelný stav- zlo... Skutečné zlo. Programy, které se spoléhají na globálně mutovatelný stav, je poměrně obtížné uvažovat a obecně jsou nespolehlivé, protože existuje příliš mnoho způsobů, jak změnit stav. Bylo provedeno mnoho studií, které tento obecný princip potvrzují, existuje nespočet návrhových vzorů, jejichž hlavním cílem je implementovat tak či onak skrývání dat. Chcete -li, aby byly vaše programy předvídatelnější, pokuste se co nejvíce eliminovat proměnlivý stav.

V sériovém programu s jedním vláknem je pravděpodobnost poškození dat přímo úměrná počtu komponent, které mohou data upravovat.

Globálního stavu není zpravidla možné úplně zbavit, ale vývojář má ve svém arzenálu velmi účinné nástroje, které vám umožňují přísně kontrolovat, které programové součásti mohou stav změnit. Kromě toho jsme se naučili vytvářet restriktivní vrstvy API kolem primitivních datových struktur. Proto máme dobrou kontrolu nad tím, jak se tyto datové struktury mění.

Problémy globálně proměnlivého stavu se postupně začaly projevovat koncem 80. a začátkem 90. let s rozšířením programování řízeného událostmi. Programy již nezačínaly „od začátku“ nebo sledovaly jedinou předvídatelnou cestu provedení „až do konce“. Moderní programy mají počáteční stav, po jehož ukončení se v nich vyskytují události - v nepředvídatelném pořadí, s proměnlivými časovými intervaly. Kód zůstane s jedním vláknem, ale již se stane asynchronní. Pravděpodobnost poškození dat se zvyšuje právě proto, že pořadí událostí je velmi důležité. Situace tohoto druhu jsou zcela běžné: pokud nastane událost B po události A, pak vše funguje dobře. Pokud ale událost A nastane po události B a událost C má čas zasáhnout mezi nimi, pak mohou být data zkreslena k nepoznání.

Pokud jsou zahrnuty paralelní toky, problém se dále zhoršuje, protože v globálním stavu může současně fungovat několik metod. Je nemožné posoudit, jak přesně se globální stav mění. Už mluvíme nejen o tom, že události mohou nastat v nepředvídatelném pořadí, ale také o tom, že lze aktualizovat stav několika podprocesů. zároveň... S asynchronním programováním můžete přinejmenším zajistit, aby se určitá událost nemohla stát, dokud jiná událost nedokončí zpracování. To znamená, že je možné s jistotou říci, jaký bude globální stav na konci zpracování konkrétní události. U vícevláknového kódu je zpravidla nemožné určit, které události budou probíhat souběžně, takže nelze s jistotou popsat globální stav v každém okamžiku.

Vícevláknový program s rozsáhlým globálně měnitelným stavem je jedním z nejvýstižnějších příkladů Heisenbergova principu nejistoty, o kterém vím. Bez změny jeho chování není možné zkontrolovat stav programu.

Když spustím další filipínštinu o globálním proměnlivém stavu (podstata je nastíněna v předchozích několika odstavcích), programátoři protočí očima a ujišťují mě, že to všechno už dávno vědí. Ale pokud to víte, proč to nemůžete poznat z kódu? Programy jsou přeplněny globálním měnitelným stavem a programátoři se diví, proč kód nefunguje.

Není překvapením, že nejdůležitější práce ve vícevláknovém programování probíhá ve fázi návrhu. Je nutné jasně definovat, co by měl program dělat, vyvinout nezávislé moduly pro provádění všech funkcí, podrobně popsat, jaká data jsou pro který modul požadována, a určit způsoby výměny informací mezi moduly ( Ano, nezapomeňte připravit pěkná trička všem, kteří se na projektu podílejí. První věc.- Cca. vyd. v originále). Tento proces se zásadně neliší od návrhu programu s jedním vláknem. Klíčem k úspěchu, stejně jako u kódu s jedním vláknem, je omezení interakcí mezi moduly. Pokud se můžete zbavit sdíleného proměnlivého stavu, problémy se sdílením dat jednoduše nevzniknou.

Někdo by mohl namítnout, že někdy není čas na tak delikátní design programu, který umožní obejít se bez globálního stavu. Věřím, že je možné a nutné tomu věnovat čas. Nic neovlivňuje vícevláknové programy tak destruktivně jako snaha vyrovnat se s globálním měnitelným stavem. Čím více podrobností musíte spravovat, tím je pravděpodobnější, že váš program dosáhne vrcholu a dojde k chybě.

V realistických aplikacích musí existovat určitý sdílený stav, který se může změnit. A tady začíná mít většina programátorů problémy. Programátor vidí, že je zde vyžadován sdílený stav, obrátí se k vícevláknovému arzenálu a vezme si odtud nejjednodušší nástroj: univerzální zámek (kritická sekce, mutex nebo jak tomu říkají). Zdá se, že věří, že vzájemné vyloučení vyřeší všechny problémy se sdílením dat.

Počet problémů, které mohou nastat s takovým jediným zámkem, je ohromující. Je třeba vzít v úvahu podmínky závodu, problémy s bránou s příliš rozsáhlým blokováním a otázky spravedlnosti přidělování jsou jen několika příklady. Pokud máte více zámků, zejména pokud jsou vnořené, budete také muset provést opatření proti zablokování, dynamickému zablokování, blokovacím frontám a dalším hrozbám spojeným se souběžností. Kromě toho existují inherentní problémy s jedním blokováním.
Když píšu nebo kontroluji kód, mám téměř neomylné železné pravidlo: pokud jste udělali zámek, pak se zdá, že jste někde udělali chybu.

Toto prohlášení lze komentovat dvěma způsoby:

  1. Pokud potřebujete zamykání, pravděpodobně máte globální mutovatelný stav, který chcete chránit před souběžnými aktualizacemi. Přítomnost globálního měnitelného stavu je chybou ve fázi návrhu aplikace. Zkontrolovat a přepracovat.
  2. Správné používání zámků není snadné a lokalizovat chyby související se zamykáním může být neuvěřitelně obtížné. Je velmi pravděpodobné, že zámek použijete nesprávně. Pokud vidím zámek a program se chová neobvyklým způsobem, pak první věc, kterou udělám, je zkontrolovat kód, který závisí na zámku. A obvykle v tom nacházím problémy.

Oba tyto výklady jsou správné.

Psaní vícevláknového kódu je snadné. Je však velmi, velmi obtížné správně používat synchronizační primitiva. Možná nejste kvalifikovaní správně používat ani jeden zámek. Koneckonců, zámky a další synchronizační primitiva jsou konstrukce, které jsou postaveny na úrovni celého systému. Lidé, kteří chápou paralelní programování mnohem lépe než vy, používají tato primitiva k vytváření souběžných datových struktur a synchronizačních konstrukcí na vysoké úrovni. A ty a já, obyčejní programátoři, prostě bereme takové konstrukce a používáme je v našem kódu. Aplikační programátor by neměl používat primitiva synchronizace na nízké úrovni častěji, než přímo volá ovladače zařízení. Tedy téměř nikdy.

Pokoušet se používat zámky k řešení problémů se sdílením dat je jako uhasit oheň kapalným kyslíkem. Podobně jako požáru je jednodušší těmto problémům předcházet, než je řešit. Pokud se zbavíte sdíleného stavu, nemusíte ani zneužívat synchronizační primitivy.

Většina toho, co víte o multithreadingu, je irelevantní

V tutoriálech pro více vláken pro začátečníky se dozvíte, co jsou vlákna. Poté autor začne zvažovat různé způsoby, jak mohou tato vlákna fungovat souběžně - například mluvit o řízení přístupu ke sdíleným datům pomocí zámků a semaforů, zabývat se tím, co se může při práci s událostmi stát. Podrobně se podíváme na proměnné podmínek, paměťové bariéry, kritické sekce, mutexy, volatilní pole a atomové operace. Budou diskutovány příklady toho, jak používat tyto nízkoúrovňové konstrukce k provádění všech druhů systémových operací. Po přečtení tohoto materiálu na polovinu se programátor rozhodne, že už o všech těchto primitivech a jejich použití ví dost. Koneckonců, pokud vím, jak tato věc funguje na systémové úrovni, mohu ji použít stejným způsobem na aplikační úrovni. Ano?

Představte si, že řeknete teenagerovi, jak si sám sestaví spalovací motor. Pak ho bez jakéhokoli školení v řízení posadíte za volant auta a řeknete: „Běž!“ Teenager rozumí tomu, jak auto funguje, ale netuší, jak se na něm dostat z bodu A do bodu B.

Pochopení toho, jak vlákna fungují na systémové úrovni, obvykle na úrovni aplikace nijak nepomůže. Nenaznačuji, že by se programátoři nemuseli učit všechny tyto detaily na nízké úrovni. Jen nečekejte, že budete moci tyto znalosti uplatnit hned při návrhu nebo vývoji obchodní aplikace.

Úvodní vláknová literatura (a související akademické kurzy) by neměla zkoumat takové nízkoúrovňové konstrukce. Musíte se zaměřit na řešení nejběžnějších tříd problémů a ukázat vývojářům, jak jsou tyto problémy řešeny pomocí funkcí na vysoké úrovni. V zásadě je většina podnikových aplikací extrémně jednoduchými programy. Načtou data z jednoho nebo více vstupních zařízení, provedou na těchto datech nějaké složité zpracování (například v procesu si vyžádají další data) a poté vydají výsledky.

Takové programy často perfektně zapadají do modelu poskytovatele a spotřebitele, který vyžaduje pouze tři vlákna:

  • vstupní proud čte data a zařazuje je do vstupní fronty;
  • pracovní vlákno čte záznamy ze vstupní fronty, zpracovává je a výsledky vkládá do výstupní fronty;
  • výstupní proud čte položky z výstupní fronty a ukládá je.

Tato tři vlákna fungují nezávisle, komunikace mezi nimi probíhá na úrovni fronty.

Zatímco technicky lze tyto fronty považovat za zóny sdíleného stavu, v praxi jsou to jen komunikační kanály, ve kterých funguje jejich vlastní vnitřní synchronizace. Fronty podporují práci s mnoha producenty a spotřebiteli najednou, můžete do nich souběžně přidávat a odebírat položky.

Protože jsou fáze vstupu, zpracování a výstupu navzájem izolované, lze jejich implementaci snadno změnit, aniž by to ovlivnilo zbytek programu. Dokud se typ dat ve frontě nezmění, můžete jednotlivé součásti programu refaktorovat podle svého uvážení. Navíc vzhledem k tomu, že se fronty účastní libovolný počet dodavatelů a spotřebitelů, není obtížné přidat další výrobce / spotřebitele. Můžeme mít desítky vstupních toků, které zapisují informace do stejné fronty, nebo desítky pracovních vláken, které berou informace ze vstupní fronty a zpracovávají data. V rámci jednoho počítače se takový model dobře přizpůsobuje.

A co je nejdůležitější, moderní programovací jazyky a knihovny velmi usnadňují vytváření aplikací pro spotřebitele. V .NET najdete Parallel Collections a knihovnu toku dat TPL. Java má službu Executor a také BlockingQueue a další třídy z oboru názvů java.util.concurrent. C ++ má knihovnu vláken Boost a knihovnu Intel Thread Building Blocks. Microsoft Visual Studio 2013 zavádí asynchronní agenty. Podobné knihovny jsou k dispozici také v jazycích Python, JavaScript, Ruby, PHP a pokud vím, v mnoha dalších jazycích. Pomocí kteréhokoli z těchto balíčků můžete vytvořit aplikaci producent-spotřebitel, aniž byste se museli uchýlit k zámkům, semaforům, proměnným podmínkám nebo jakýmkoli jiným synchronizačním primitivům.

V těchto knihovnách se volně používá široká škála synchronizačních primitiv. Tohle je fajn. Všechny tyto knihovny jsou napsány lidmi, kteří multithreadingu rozumějí nesrovnatelně lépe než průměrný programátor. Práce s takovou knihovnou je prakticky stejná jako používání knihovny runtime jazyků. To lze přirovnat k programování v jazyce vyšší úrovně než v jazyce sestavení.

Model dodavatel-spotřebitel je jen jedním z mnoha příkladů. Výše uvedené knihovny obsahují třídy, které lze použít k implementaci mnoha běžných návrhových vzorců vláken, aniž byste museli jít do podrobností nízké úrovně. Je možné vytvářet rozsáhlé vícevláknové aplikace bez obav z koordinace a synchronizace vláken.

Práce s knihovnami

Vytváření vícevláknových programů se tedy zásadně neliší od psaní synchronních programů s jedním vláknem. Důležité principy zapouzdření a skrývání dat jsou univerzální a nabývají na důležitosti, pouze pokud je zapojeno více souběžných vláken. Pokud tyto důležité aspekty zanedbáte, pak vás ani nejkomplexnější znalost nízkoúrovňových vláken nezachrání.

Moderní vývojáři musí vyřešit spoustu problémů na úrovni programování aplikací, stává se, že zkrátka není čas přemýšlet o tom, co se děje na systémové úrovni. Čím jsou aplikace složitější, tím složitější detaily musí být skryty mezi úrovněmi API. Děláme to více než tucet let. Lze tvrdit, že kvalitativní skrývání složitosti systému před programátorem je hlavním důvodem, proč je programátor schopen psát moderní aplikace. Co se toho týče, neskrýváme složitost systému implementací smyčky zpráv UI, vytvářením komunikačních protokolů na nízké úrovni atd.?

Podobná situace je u multithreadingu. Většina scénářů s více vlákny, se kterými se průměrný programátor podnikových aplikací může setkat, je již dobře známá a dobře implementovaná v knihovnách. Funkce knihovny skvěle odkrývají ohromující složitost paralelismu. Musíte se naučit používat tyto knihovny stejným způsobem, jakým používáte knihovny prvků uživatelského rozhraní, komunikační protokoly a řadu dalších nástrojů, které prostě fungují. Ponechejte víceúrovňové zpracování na nízké úrovni na odbornících - autorech knihoven používaných při tvorbě aplikací.

NS Tento článek není pro zkušené krotitele Pythonu, pro které je rozmotání této koule hadů dětskou hrou, ale spíše povrchní přehled vícevláknových schopností pro nově závislý python.

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


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

Vícevláknové aplikace


Python má modul navlékání , a má vše, co potřebujete pro vícevláknové programování: existují různé typy zámků, semafor a mechanismus událostí. Jedním slovem - vše, co je potřeba pro drtivou většinu vícevláknových programů. Kromě toho je použití všech těchto nástrojů velmi jednoduché. Uvažujme příklad programu, který spouští 2 vlákna. Jedno vlákno zapisuje deset „0“, druhé - deset „1“ a postupně.

importování vláken

def spisovatel

pro i v xrange (10):

tisknout x

Event_for_set.set ()

# inicializační události

e1 = threading.Event ()

e2 = threading.Event ()

# inicializační vlákna

0, e1, e2))

1, e2, e1))

# spusťte vlákna

t1.start ()

t2.start ()

t1.join ()

t2.join ()


Žádné kouzlo ani voodoo kód. Kód je jasný a konzistentní. Navíc, jak vidíte, vytvořili jsme stream z funkce. To je velmi výhodné pro malé úkoly. Tento kód je také velmi flexibilní. Předpokládejme, že máme třetí proces, který zapíše „2“, pak bude kód vypadat takto:

importování vláken

def spisovatel (x, event_for_wait, event_for_set):

pro i v xrange (10):

Event_for_wait.wait () # čekání na událost

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

tisknout x

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

# inicializační události

e1 = threading.Event ()

e2 = threading.Event ()

e3 = threading.Event ()

# inicializační vlákna

t1 = threading.Thread (cíl = zapisovatel, args = ( 0, e1, e2))

t2 = threading.Thread (cíl = zapisovatel, args = ( 1, e2, e3))

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

# spusťte vlákna

t1.start ()

t2.start ()

t3.start ()

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

# připojte 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ě změnili parametry, pomocí kterých
proudy začínají (obecnější řešení můžete samozřejmě napsat například pomocí MapReduce, ale to je mimo rozsah tohoto článku).
Jak vidíte, magie stále neexistuje. Všechno je jednoduché a přímočaré. Pojďme dále.

Globální zámek 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 na paralelní, plně nebo částečně asynchronní sekce (například abychom mohli pingovat několik serverů současně).

V prvním případě stojíme před takovým omezením Pythonu (nebo spíše jeho hlavní implementací CPython) jako Global Interpreter Lock (zkráceně GIL). Koncept GIL spočívá v tom, že procesor může současně provádět pouze jedno vlákno. To se děje tak, že neexistuje žádný boj mezi vlákny pro samostatné proměnné. Spustitelné vlákno získá přístup k celému prostředí. Tato funkce implementace vláken v Pythonu výrazně zjednodušuje práci s vlákny a poskytuje jistou bezpečnost vláken.

Existuje však jemný bod: může se zdát, že vícevláknová aplikace poběží přesně stejně dlouho jako aplikace s jedním vláknem, která dělá totéž, nebo součet doby provedení každého vlákna na CPU. Zde nás ale čeká jeden nepříjemný efekt. Zvažte program:

s otevřeným ("test1.txt", "w") jako fout:

pro i v xrange (10 000 000):

tisk >> fout, 1


Tento program zapíše do souboru milion řádků „1“ a na mém počítači to provede za ~ 0,35 sekundy.

Zvažte jiný program:

z vlákna importovat vlákno

def spisovatel (název souboru, n):

s otevřeným (název souboru, "w") jako fout:

pro i v xrange (n):

tisk >> fout, 1

t1 = vlákno (cíl = zapisovatel, args = ("test2.txt", 500 000,))

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

t1.start ()

t2.start ()

t1.join ()

t2.join ()


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

To je způsobeno skutečností, že když vlákno nepotřebuje zdroj CPU, uvolní GIL a v tuto chvíli se může pokusit jej získat a další vlákno a také hlavní vlákno. 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ádry.

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

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

Multiprocesní aplikace


Aby bylo možné v jistém smyslu vyřešit problém popsaný v předchozím odstavci, má Python modul podproces ... Můžeme napsat program, který chceme spustit v paralelním vlákně (ve skutečnosti již proces). A spusťte jej v jednom nebo více vláknech v jiném programu. To by náš program opravdu zrychlilo, protože vlákna vytvořená ve spouštěči GIL nezvedají, ale pouze čekají na dokončení spuštěného procesu. Tato metoda má však spoustu problémů. Hlavním problémem je, že je obtížné přenášet data mezi procesy. Museli byste nějak serializovat objekty, navázat komunikaci pomocí PIPE nebo jiných nástrojů, ale to vše nevyhnutelně nese režii a kód se stává obtížně pochopitelným.

Zde nám může pomoci jiný přístup. Python má modul pro více procesů ... Pokud jde o funkčnost, tento modul se podobá navlékání ... Například procesy lze vytvářet stejným způsobem z běžných funkcí. Metody pro práci s procesy jsou téměř stejné jako pro vlákna z modulu vláken. Pro synchronizaci procesů a výměnu dat je však obvyklé používat jiné nástroje. Mluvíme o frontách (Queue) a potrubí (Pipe). Jsou zde však také analogie zámků, událostí a semaforů, které byly ve vytváření vláken.

Víceprocesní modul má navíc mechanismus pro práci se sdílenou pamětí. K tomu má modul třídy proměnné (hodnota) a pole (pole), které lze „sdílet“ mezi procesy. Pro usnadnění práce se sdílenými proměnnými můžete použít manažerské třídy. Jsou flexibilnější a snadněji se používají, ale pomaleji. Je třeba poznamenat, že existuje pěkná příležitost vytvořit běžné typy z modulu ctypes pomocí modulu multiprocessing.sharedctypes.

Také v modulu pro více procesů existuje mechanismus pro vytváření fondů procesů. Tento mechanismus je velmi vhodné použít k implementaci vzoru Master-Worker nebo k implementaci paralelní mapy (což je v jistém smyslu speciální případ Master-Worker).

Z hlavních problémů při práci s víceprocesním modulem stojí za zmínku relativní závislost platformy na tomto 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 Windows nemá 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...


Existují další knihovny a přístupy pro psaní paralelních aplikací v Pythonu. Můžete například použít Hadoop + Python nebo různé implementace MPI Pythonu (pyMPI, mpi4py). Můžete dokonce použít obaly stávajících knihoven C ++ nebo Fortran. Zde bychom mohli zmínit takové rámce / knihovny jako Pyro, Twisted, Tornado a mnoho dalších. To vše už ale přesahuje rámec tohoto článku.

Pokud se vám můj styl líbil, tak se vám v dalším článku pokusím sdělit, jak v PLY psát jednoduché tlumočníky a k čemu je lze použít.

Kapitola 10.

Vícevláknové aplikace

Multitasking v moderních operačních systémech je samozřejmostí [ Před příchodem systému Apple OS X neexistovaly na počítačích Macintosh žádné moderní víceúlohové operační systémy. Je velmi obtížné správně navrhnout operační systém s plným multitaskingem, takže OS X musel být založen na Unixu.]. Uživatel očekává, že při současném spuštění textového editoru a poštovního klienta nedojde ke konfliktu těchto programů a při příjmu e-mailu editor nepřestane fungovat. Když je spuštěno několik programů současně, operační systém rychle přepíná mezi programy a poskytuje jim postupně procesor (pokud ovšem v počítači není nainstalováno více procesorů). Jako výsledek, iluze spouštění více programů současně, protože ani nejlepší písař (a nejrychlejší připojení k internetu) nedokáže držet krok s moderním procesorem.

Multithreading, v jistém smyslu, může být viděn jako další úroveň multitaskingu: mí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. E-mailový klient s více vlákny například umožňuje přijímat nové e-mailové zprávy při čtení nebo psaní nových zpráv. V dnešní době je mnoho uživatelů také považováno za samozřejmé.

VB nikdy neměla normální podporu více vláken. Je pravda, že jedna z jejích odrůd se objevila ve VB5 - kolaborativní streamovací model(provlékání bytů). Jak brzy uvidíte, kolaborativní model poskytuje programátorovi některé výhody multithreadingu, ale nevyužívá všechny výhody všech funkcí. Dříve nebo později budete muset přejít z tréninkového stroje na skutečný a VB .NET se stala první verzí VB s podporou bezplatného vícevláknového modelu.

Multithreading však nepatří mezi funkce, které lze snadno implementovat do programovacích jazyků a které programátoři snadno zvládnou. Proč?

Protože ve vícevláknových aplikacích mohou nastat velmi záludné chyby, které se objevují a mizí nepředvídatelně (a takové chyby se nejobtížněji ladí).

Upřímně varování: multithreading je jednou z nejtěžších oblastí programování. Sebemenší nepozornost vede ke vzniku nepolapitelných chyb, jejichž oprava vyžaduje astronomické částky. Z tohoto důvodu obsahuje tato kapitola mnoho špatný příklady - záměrně jsme je napsali tak, aby demonstrovali běžné chyby. Toto je nejbezpečnější přístup k učení vícevláknového programování: musíte být schopni rozpoznat 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 vícevláknové programovací techniky, neobejdete se bez toho.

Tato kapitola položí pevný základ pro další nezávislou práci, ale nebudeme schopni popsat vícevláknové programování ve všech složitostech - pouze tištěná dokumentace o třídách oboru názvů Threading trvá více než 100 stran. Pokud chcete zvládnout 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í vícevláknové zpracování, budou uživatelé velmi frustrovaní a upřednostňují jiný produkt. Například pouze ve čtvrté verzi populárního e-mailového programu se Eudora objevila vícevláknové schopnosti, bez nichž si nelze představit žádný moderní program pro práci s e-mailem. V době, kdy Eudora zavedla podporu více vláken, mnoho uživatelů (včetně jednoho z autorů této knihy) přešlo na jiné produkty.

Nakonec v .NET programy s jedním vláknem jednoduše neexistují. Všechno Programy .NET jsou vícevláknové, protože uvolňování paměti běží jako proces na pozadí s nízkou prioritou. Jak je ukázáno níže, u seriózního grafického programování v .NET může správné navlékání pomoci zabránit blokování grafického rozhraní, když program provádí zdlouhavé operace.

Představujeme multithreading

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

Ukládání kontextu s sebou nese náklady na čas a paměť. Operační systém si pamatuje stav podprocesu programu a přenáší řízení na jiné vlákno. Když chce program pokračovat ve provádění pozastaveného vlákna, musí být uložený kontext obnoven, což trvá ještě déle. Multithreading by proto měl být používán pouze tehdy, když přínosy kompenzují všechny náklady. Některé typické příklady jsou uvedeny níže.

  • Funkčnost programu je jasně a přirozeně rozdělena do několika heterogenních operací, jako v příkladu s přijímáním e-mailů a přípravou nových zpráv.
  • Program provádí dlouhé a složité výpočty a vy nechcete, aby bylo grafické rozhraní po dobu výpočtů blokováno.
  • 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řesáhne počet procesorů, paralelní provádění je prakticky bez nákladů spojených s přepínáním vláken).

Předtím, než přejdeme k mechanice 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 vyvolává zmatek.

V toku programu se provádí procedura, nikoli objekt.

Je těžké říci, co se rozumí výrazem „objekt se spouští“, 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í. Možná si někdo myslí, že práce podprocesu 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, která provádějí různé (a někdy dokonce stejné) metody, zatímco zprávy objektu jsou přenášeny a přijímány několika různými vlákny (mimochodem, toto je jeden z důvodů, které komplikují vícevláknové programování: abyste mohli ladit program, musíte zjistit, které vlákno v daném okamžiku provádí ten či onen postup!).

Protože vlákna jsou vytvářena z metod objektů, je samotný objekt 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ává příkaz k zahájení provádění vlákna. Procedura, pro kterou bylo vlákno vytvořeno, může stejně jako všechny procedury vytvářet nové objekty, provádět operace se stávajícími objekty a volat další procedury a funkce, které jsou v jejím rozsahu.

Běžné metody tříd lze také provádět ve vláknech programu. V tomto případě mějte také na paměti další důležitou okolnost: vlákno končí odchodem z postupu, pro který bylo vytvořeno. Normální dokončení toku programu není možné, dokud proceduru neopustíte.

Vlákna mohou být ukončena nejen přirozeně, ale také abnormálně. To se obecně nedoporučuje. Další informace najdete v části Ukončení a přerušení streamů.

Základní nástroje .NET související s používáním programových vláken jsou soustředěny do oboru názvů Threading. Většina vícevláknových programů by proto měla začínat na následujícím řádku:

Importuje System.Threading

Import oboru názvů usnadňuje psaní programu a umožňuje technologii IntelliSense.

Přímé propojení toků s postupy naznačuje, že na tomto obrázku delegáti(viz kapitola 6). Konkrétně jmenný prostor Threading obsahuje delegáta ThreadStart, který se obvykle používá při spouštění podprocesů programu. Syntaxe pro použití tohoto delegáta vypadá takto:

Veřejný delegát Sub ThreadStart ()

Kód volaný s delegátem ThreadStart nesmí mít žádné parametry ani návratovou hodnotu, takže vlákna nelze vytvářet pro funkce (které vracejí hodnotu) a pro procedury s parametry. Chcete -li přenášet informace ze streamu, musíte také hledat alternativní prostředky, protože provedené metody nevracejí hodnoty a nemohou používat přenos podle odkazu. Pokud je například ThreadMethod ve třídě WilluseThread, pak ThreadMethod může sdělovat informace úpravou vlastností instancí třídy WillUseThread.

Aplikační domény

Vlákna .NET běží v takzvaných aplikačních doménách, definovaných v dokumentaci jako „sandbox, ve kterém aplikace běží“. Doménu aplikace lze považovat za odlehčenou verzi procesů Win32; jeden proces Win32 může obsahovat více domén aplikace. Hlavní rozdíl mezi aplikačními doménami a procesy spočívá v tom, že proces Win32 má svůj vlastní adresní prostor (v dokumentaci jsou aplikační domény také porovnávány s logickými procesy běžícími uvnitř fyzického procesu). V NET je veškerá správa paměti řešena za běhu, takže v jednom procesu Win32 lze spustit více domén aplikace. Jednou z výhod tohoto schématu jsou vylepšené možnosti škálování aplikací. Nástroje pro práci s aplikačními doménami jsou ve třídě AppDomain. Doporučujeme prostudovat dokumentaci k této třídě. S jeho pomocí můžete získat informace o prostředí, ve kterém váš program běží. Zejména třída AppDomain se používá při provádění reflexe na systémových třídách .NET. Následující program uvádí načtené sestavy.

Importuje System.Reflection

Modul Modulel

Vedlejší ()

Dim the Domain As AppDomain

theDomain = AppDomain.CurrentDomain

Dim Assemblies () As

Assemblies = theDomain.GetAssemblies

Dim anAssemblyxAs

Pro každou sestavu v sestavách

Console.WriteLinetanAssembly.Full Name) Další

Console.ReadLine ()

End Sub

Koncový modul

Vytváření streamů

Začněme na základním příkladu. Řekněme, že chcete spustit proceduru v samostatném vlákně, které sníží hodnotu čítače v nekonečné smyčce. Postup je definován jako součást třídy:

Veřejná třída WillUseThreads

Veřejné subtractFromCounter ()

Počet dim jako celé číslo

Počítejte, když je to pravda - = 1

Console.WriteLlne ("Jsem v jiném vlákně a čítač ="

& počet)

Smyčka

End Sub

Koncová třída

Protože podmínka cyklu Do je vždy pravdivá, můžete si myslet, že nic nebude zasahovat do procedury SubtractFromCounter. Ve vícevláknové aplikaci to však neplatí vždy.

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 Modulel

Vedlejší ()

1 Dim myTest jako nový WillUseThreads ()

2 Dim bThreadStart jako nový ThreadStart (AddressOf _

myTest.SubtractFromCounter)

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

4 "bThread.Start ()

Dim i As Integer

5 Dělejte, když je to pravda

Console.WriteLine („V hlavním vlákně a počet je“ & i) i + = 1

Smyčka

End Sub

Koncový modul

Pojďme se podívat na nejdůležitější body v pořadí. Za prvé, postup Sub Man n vždy funguje hlavní proud(hlavní vlákno). V programech .NET jsou vždy spuštěna alespoň dvě vlákna: hlavní vlákno a podproces kolekce odpadků. Řádek 1 vytvoří novou instanci testovací třídy. V řádku 2 vytvoříme delegáta ThreadStart a předáme adresu procedury SubtractFromCounter instanci testovací třídy vytvořené v řádku 1 (tato procedura se nazývá bez parametrů). DobrýImportem oboru názvů Threading lze dlouhý název vynechat. Nový objekt vlákna je vytvořen na řádku 3. Všimněte si předávání delegáta ThreadStart při volání konstruktoru třídy Thread. Někteří programátoři dávají přednost spojování těchto dvou řádků do jednoho logického řádku:

Dim bThread jako nové vlákno (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 říkáme operačnímu systému, že procedura Subtract by měla běžet v samostatném vlákně.

Slovo „začíná“ v předchozím odstavci je uzavřeno v uvozovkách, protože toto je jedna z mnoha zvláštností vícevláknového programování: Volání Start ve skutečnosti vlákno nezačne! Pouze říká operačnímu systému, aby naplánoval spuštění zadaného vlákna, ale je mimo kontrolu programu, aby se spustil přímo. Spouštění vláken nebudete moci spustit sami, protože provádění vláken vždy řídí operační systém. V pozdější části se naučíte, jak použít prioritu, aby operační systém spustil vaše vlákno rychleji.

Na obr. 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ě nové vlákno začalo až poté, co se čítač v hlavním vlákně zvýšil na 341!

Rýže. 10.1. Jednoduché vícevláknové běhové prostředí softwaru

Pokud program poběží delší dobu, bude výsledek vypadat podobně jako na obrázku. 10.2. Vidíme, že tydokončení běžícího vlákna se pozastaví a kontrola se znovu přenese do hlavního vlákna. V tomto případě existuje projev preemptivní multithreading prostřednictvím časového krájení. Význam tohoto děsivého pojmu 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šení vláken a přenosu řízení na jiná vlákna používá operační systém princip preemptivního multithreadingu prostřednictvím časového krájení. Kvantizace času také řeší jeden z běžných problémů, které dříve vyvstávaly ve vícevláknových programech - jedno vlákno zabere veškerý čas CPU a není horší než ovládání ostatních vláken (zpravidla se to děje v intenzivních cyklech, jako je ten výše). Aby se zabránilo exkluzivnímu únosu CPU, měla by vaše vlákna čas od času přenášet kontrolu na jiná vlákna. Pokud se program ukáže jako „nevědomý“, existuje další, o něco méně žádoucí řešení: operační systém vždy předchází spuštěnému vláknu bez ohledu na jeho úroveň priority, takže přístup k procesoru je udělen každému vláknu v systému.

Vzhledem k tomu, že kvantizační schémata všech verzí systému Windows, na kterých běží .NET, mají pro každé vlákno minimální časový úsek, v programování .NET nejsou problémy s chycením exkluzivního procesoru tak závažné. Na druhou stranu, pokud je .NET framework někdy přizpůsoben pro jiné systémy, může se to 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í zlomek času 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é k dispozici pro vlákna s nižší prioritou

Příkaz přiřadí novému vláknu maximální prioritu a sníží prioritu hlavního vlákna. Obr. 10.3 je vidět, že nové vlákno začíná pracovat rychleji než dříve, ale, jak ukazuje obr. 10.4, hlavní vlákno také dostává kontrolulenost (byť na velmi krátkou dobu a až po delší práci toku s odečtením). Když spustíte program na svých počítačích, získáte výsledky podobné těm, které jsou uvedeny na obr. 10.3 a 10.4, ale vzhledem k rozdílům mezi našimi systémy nebude přesná shoda.

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

Nit Priorita. Nejvyšší

ThreadPriority.AboveNormal

ThreadPrlority.Normal

ThreadPriority.BlowNormal

Vlákno Priorita Nejnižší

Metoda připojení

Někdy je třeba vlákno programu pozastavit, dokud nedokončí jiné vlákno. Řekněme, že chcete pozastavit vlákno 1, dokud vlákno 2 nedokončí svůj výpočet. Pro tohle z proudu 1 metoda Stream je volána pro stream 2. Jinými slovy, příkaz

vlákno 2. Připojte se ()

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

Pokud se připojíte k proudu 1 k proudu 2 pomocí metody Join, operační systém automaticky spustí stream 1 po proudu 2. Všimněte si, že proces spouštění je nedeterministické: nelze přesně říci, jak dlouho po skončení vlákna 2 začne fungovat vlákno 1. Existuje další verze spojení, která vrací booleovskou hodnotu:

vlákno 2. Připojte se (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 přidělí vláknu opět čas CPU. Metoda vrací True, pokud vlákno 2 skončí před vypršením zadaného časového limitu, a False jinak.

Pamatujte na základní pravidlo: zda vlákno 2 bylo dokončeno nebo vypršel časový limit, nemáte nad aktivací vlákna 1 žádnou kontrolu.

Názvy vláken, CurrentThread a ThreadState

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

Ačkoli existuje nádherné vlákno pro ladění vícevláknových aplikací ve VB .NET, které je popsáno níže, velmi často nám pomohl tento příkaz

MsgBox (Thread.CurrentThread.Name)

Často se ukázalo, že kód se spouští ve zcela jiném vláknu, ze kterého měl být spuštěn.

Připomeňme, že termín „nedeterministické plánování toků programů“ znamená velmi jednoduchou věc: programátor prakticky nemá k dispozici žádné prostředky, které by ovlivňovaly 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 streamů

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

bThread.Name = "Odečtení vlákna"

Přibližný pohled na okno streamů 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 streamů

Šipka v prvním sloupci označuje aktivní vlákno vrácené vlastností Thread.CurrentThread. Sloupec ID obsahuje ID číselných vláken. V dalším sloupci jsou uvedeny názvy streamů (pokud jsou přiřazeny). Sloupec Umístění označuje postup, který se má spustit (například procedura WriteLine třídy Console na obrázku 10.5). Zbývající sloupce obsahují informace o prioritních a pozastavených vláknech (viz další část).

Okno vlákna (nikoli operační systé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 pravým tlačítkem na odpovídající řádek a výběrem příkazu Zmrazit (později lze zastavené vlákno obnovit). Zastavení vláken se často používá při ladění, aby se zabránilo nesprávnému fungování vlákna, které by zasahovalo do aplikace. Okno streamů vám navíc umožňuje aktivovat další (nezastavený) stream; Chcete-li to provést, klepněte pravým tlačítkem na požadovaný řádek a v místní nabídce vyberte příkaz Přepnout na vlákno (nebo jednoduše dvakrát klikněte na řádek vlákna). Jak bude ukázáno níže, je to velmi užitečné při diagnostice potenciálních zablokování.

Pozastavení streamu

Dočasně nepoužívané streamy lze přenést do pasivního stavu pomocí metody Slеer. Pasivní stream je také považován za zablokovaný. Samozřejmě, když je vlákno uvedeno do pasivního stavu, zbytek vláken bude mít více prostředků procesoru. Standardní syntaxe metody Slеer je následující: Thread.Sleep (interval_in_milliseconds)

V důsledku volání režimu spánku se aktivní vlákno stane pasivním po dobu alespoň zadaného počtu milisekund (aktivace bezprostředně po uplynutí zadaného intervalu však není zaručena). Poznámka: při volání metody není předán odkaz na konkrétní vlákno - metoda Sleep je volána pouze pro aktivní vlákno.

Další verze režimu spánku způsobí, že se aktuální vlákno vzdá zbytku přiděleného času CPU:

Thread.Sleep (0)

Další 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.Slеer (Timeout.Infinite)

Protože pasivní vlákna (i s neomezeným časovým limitem) lze přerušit metodou Přerušení, což ve výjimečných případech vede k zahájení ThreadlnterruptExcepti, volání Slayer je vždy uzavřeno v bloku Try-Catch, jako v následujícím úryvku:

Snaž se

Thread.Sleep (200)

„Pasivní stav vlákna byl přerušen

Chytit jako výjimku

„Jiné výjimky

Konec Zkuste

Každý program .NET běží na podprocesu programu, takže metoda Sleep se používá také k pozastavení programů (pokud obor názvů Threadipg program neimportuje, musíte použít plně kvalifikovaný název Threading.Thread. Sleep).

Ukončení nebo přerušení vláken programu

Vlákno se automaticky ukončí, když se vytvoří metoda zadaná při vytvoření delegáta ThreadStart, ale někdy je nutné metodu (a tedy vlákno) ukončit, když nastanou určité faktory. V takových případech proudy obvykle kontrolují podmíněná proměnná, podle toho v jakém stavuje rozhodnuto o nouzovém východu z potoka. Do postupu je obvykle zahrnuta smyčka Do-While:

Sub ThreadedMethod ()

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

"podmíněná proměnná."

„Například podmíněnou proměnnou lze stylizovat jako vlastnost

Do While conditionVariable = False And MoreWorkToDo

„Hlavní kód

Loop End Sub

Dotaz podmíněné proměnné trvá určitou dobu. Trvalé dotazování ve smyčce byste měli používat pouze v případě, že čekáte na předčasné ukončení vlákna.

Pokud musí být proměnná podmínky zaškrtnuta na konkrétním místě, použijte příkaz If-Then ve spojení s Exit Sub uvnitř nekonečné smyčky.

Přístup k podmíněné proměnné musí být synchronizován, aby expozice z jiných vláken nenarušovala její běžné použití. Toto důležité téma je popsáno v části „Řešení potíží: Synchronizace“.

Bohužel kód pasivních (nebo jinak blokovaných) vláken není spuštěn, takže možnost s dotazováním podmíněné proměnné pro ně není vhodná. V takovém případě zavolejte metodu Přerušení na objektovou proměnnou, která obsahuje odkaz na požadované vlákno.

Metodu Přerušení lze volat pouze u vláken ve stavu Čekat, Spát nebo Připojit. Pokud zavoláte Přerušení pro vlákno, které je v jednom z uvedených stavů, pak po nějaké době vlákno začne znovu fungovat a prostředí pro spuštění spustí vlákno ThreadlnterruptedExcepti při výjimce ve vlákně. K tomu dochází, i když vlákno bylo vytvořeno jako pasivní na dobu neurčitou voláním Thread.Sleepdimeout. Nekonečný). Říkáme „po chvíli“, protože plánování vláken není deterministické. ThreadlnterruptedExcepti ve výjimce je zachycen sekcí Catch, která obsahuje výstupní kód ze stavu čekání. Sekce Catch však nemusí ukončit vlákno při přerušení - vlákno zpracovává výjimku, jak uzná za vhodné.

V .NET lze metodu přerušení volat i pro odblokovaná vlákna. V tomto případě je vlákno přerušeno při nejbližší blokaci.

Pozastavení a zabíjení vláken

Obor názvů Threading obsahuje další metody, které přerušují normální vytváření vláken:

  • Pozastavit;
  • Přerušit.

Je těžké říci, proč .NET zahrnovala podporu pro tyto metody - volání Suspend a Abort pravděpodobně způsobí, že se program stane nestabilní. Žádná z metod neumožňuje normální deinicializaci streamu. Při volání Pozastavit nebo Přerušit navíc nelze předvídat, v jakém stavu vlákno zanechá objekty po pozastavení nebo přerušení.

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

"... Když je vlákno zničeno voláním Abort, runtime vyvolá ThreadAbortException." Toto je zvláštní druh výjimky, který program nemůže zachytit. Když je vyvolána tato výjimka, modul runtime spustí všechny bloky nakonec před ukončením vlákna. Protože v blocích Nakonec lze provést jakoukoli akci, zavolejte na Připojit a zajistěte, aby byl stream zničen. "

Morální: Přerušení a pozastavení se nedoporučuje (a pokud se bez pozastavení stále neobejdete, obnovte pozastavené vlákno pomocí metody Obnovit). Vlákno můžete bezpečně ukončit pouze dotazováním synchronizované proměnné podmínky nebo voláním výše uvedené metody Přerušení.

Vlákna na pozadí (démoni)

Některá vlákna běžící na pozadí se automaticky zastaví, když se zastaví jiné součásti programu. Zejména garbage collector běží v jednom z vláken na pozadí. Vlákna na pozadí se obvykle vytvářejí pro příjem dat, ale to se provádí pouze v případě, že v jiných vláknech běží kód, který dokáže zpracovat přijatá data. Syntaxe: název streamu. 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 kódu HTML

Doporučujeme používat streamy pouze tehdy, když je funkce programu jasně rozdělena do několika operací. Dobrým příkladem je program pro extrakci HTML v kapitole 9. Naše třída dělá dvě věci: načítání dat z Amazonu a jejich zpracování. Toto je perfektní příklad situace, ve které je vícevláknové programování skutečně vhodné. Vytváříme třídy pro několik různých knih a poté analyzujeme data v různých proudech. Vytvoření nového vlákna pro každou knihu zvyš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á verze tohoto programu funguje efektivněji než verze s jedním vláknem pouze na počítači s několika procesory nebo pokud lze příjem dalších dat efektivně kombinovat s jejich analýzou.

Jak bylo uvedeno výše, ve vláknech lze spouštět pouze procedury, které nemají žádné parametry, takže budete muset v programu provést drobné změny. Níže je základní postup, přepsaný pro vyloučení parametrů:

Public Sub FindRank ()

m_Rank = ScrapeAmazon ()

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

End Sub

Protože nebudeme moci použít kombinované pole pro ukládání a načítání informací (psaní vícevláknových programů s grafickým rozhraním je popsáno v poslední části této kapitoly), program ukládá data čtyř knih do pole, jehož definice začíná takto:

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

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

Ve stejném cyklu, ve kterém jsou vytvářeny objekty AmazonRanker, jsou vytvořeny čtyři streamy:

Pro i = 0 až 3

Snaž se

theRanker = New AmazonRanker (theBook (i.0). theBookd.1))

aThreadStart = Nový ThreadStar (AddressOf theRanker.FindRan ()

aThread = Nové vlákno (aThreadStart)

aThread.Name = theBook (i.l)

aThread.Start () Chytit e jako výjimku

Console.WriteLine (e.Message)

Konec Zkuste

další

Níže je kompletní text programu:

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

Importuje System.Threading

Modul Modulel

Vedlejší ()

Dim theBook (3.1) As String

kniha (0,0) = "1893115992"

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

theBook (l.0) = "1893115291"

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

kniha (2,0) = "1893115623"

theBook (2.1) = "Úvod programátora do C #."

theBook (3.0) = "1893115593"

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

Dim i As Integer

Dim theRanker As = AmazonRanker

Dim aThreadStart ztlumit jako Threading.ThreadStart

Dim aThread as Threading.Thread

Pro i = 0 až 3

Snaž se

theRanker = Nový AmazonRankerttheBook (i.0). kniha (i.1))

aThreadStart = Nový ThreadStart (AddressOf theRanker. FindRank)

aThread = Nové vlákno (aThreadStart)

aThread.Name = theBook (i.l)

aThread.Start ()

Chytit jako výjimku

Console.WriteLlnete.Message)

Konec Zkuste Další

Console.ReadLine ()

End Sub

Koncový modul

Veřejná třída AmazonRanker

Soukromý m_URL jako řetězec

Soukromé m_Rank jako celé číslo

Soukromé m_Name jako řetězec

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

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

m_Name = Konec Sub

Public Sub FindRank () m_Rank = ScrapeAmazon ()

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

& GetRank) End Sub

Veřejné vlastnictví jen pro čtení GetRank () jako řetězec Získejte

Pokud m_Rank<>0 Pak

Vrátit CStr (m_Rank) Jinak

„Problémy

Konec If

Konec Get

Koncová vlastnost

Veřejné vlastnictví jen pro čtení GetName () jako řetězec Získat

Vrátit m_Name

Konec Get

Koncová vlastnost

Soukromá funkce ScrapeAmazon () jako celočíselný pokus

Dim theURL As New Uri (m_URL)

Dim theRequest As WebRequest

theRequest = WebRequest.Create (theURL)

Dim theResponse as WebResponse

theResponse = theRequest.GetResponse

Dim aReader as New StreamReader (theResponse.GetResponseStream ())

Dim the Data 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 ()

Funkce End Try End

Soukromá funkce analyzovat (ByVal theData jako řetězec) jako celé číslo

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

Pořadí prodejnosti:") _

+ "Pořadí prodeje Amazon.com:".Délka

Dim temp As String

Dělejte, dokud theData.Substring (Location.l) = "<" temp = temp

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

Zpět Clnt (teplota)

Koncová funkce

Koncová třída

Operace s více vlákny se běžně používají v oborech názvů .NET a I / O, takže knihovna .NET Framework pro ně poskytuje speciální asynchronní metody. Další informace o používání asynchronních metod při psaní vícevláknových programů 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í vláken - naše streamy nezměnily obecné údaje. Pokud povolíte změnu obecných údajů, potenciální chyby se začnou exponenciálně množit a je mnohem obtížnější se jich pro program zbavit. Na druhou stranu, pokud zakážete úpravu sdílených dat různými vlákny, vícevláknové programování .NET se bude jen málo lišit od omezených možností VB6.

Rádi bychom vás upozornili na malý program, který demonstruje problémy, které vznikají, aniž by zacházel do zbytečných podrobností. Tento program simuluje dům s termostatem v každé místnosti. Pokud je teplota o 5 stupňů Fahrenheita nebo více (asi 2,77 stupně Celsia) nižší než cílová teplota, nařídíme topnému systému, aby zvýšil teplotu o 5 stupňů; jinak teplota stoupne pouze o 1 stupeň. Pokud je aktuální teplota větší nebo rovna nastavené, nebude provedena žádná změna. Regulace teploty v každé místnosti se provádí samostatným průtokem se zpožděním 200 milisekund. Hlavní práce se provádí pomocí následujícího úryvku:

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

Thread.Sleep (200)

Catch tie As ThreadlnterruptedException

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

Chytit jako výjimku

„Jiné výjimky typu Try Try

mHouse.HouseTemp + - 5 "atd.

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

1 Možnost Strict On

2 Importuje systém

3 Module Modulel

4 Dílčí hlavní ()

5 Dim myHouse As New House (l0)

6 Konzola. ReadLine ()

7 End Sub

8 Koncový modul

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

10 Public Const MAX_TEMP As Integer = 75

11 Soukromý mCurTemp jako celé číslo = 55

12 soukromých pokojů m) () jako pokoj

13 Public Sub New (ByVal numOfRooms As Integer)

14 ReDim mRooms (numOfRooms = 1)

15 Dim i As Integer

16 Dim aThreadStart ztlumit jako Threading.ThreadStart

17 Dim aThread as Thread

18 Pro i = 0 až numOfRooms -1

19 Zkuste

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

21 aThreadStart - nový ThreadStart (adresa _

mRooms (i) .CheckTempInRoom)

22 aThread = Nové vlákno (aThreadStart)

23 aThread.Start ()

24 Catch E jako výjimka

25 Console.WriteLine (E.StackTrace)

26 Konec Zkuste

27 Další

28 Konec Sub

29 Public Property HouseTemp () jako celé číslo

třicet. Dostat

31 Návrat mCurTemp

32 Konec Získat

33 Set (ByVal Value As Integer)

34 mCurTemp = hodnota 35 End Set

36 Koncová vlastnost

37 Koncová třída

38 Místnost veřejné třídy

39 Soukromý mCurTemp jako celé číslo

40 Soukromé mName As String

41 Soukromý mHouse As House

42 Public Sub New (ByVal theHouse As House,

ByVal temp As Integer, ByVal roomName As String)

43 mHouse = theHouse

44 mCurTemp = teplota

45 mName = název místnosti

46 End Sub

47 Public Sub CheckTempInRoom ()

48 ChangeTeplota ()

49 End Sub

50 soukromá změna teploty sub ()

51 Zkuste

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

53 vláken. Spánek (200)

54 mHouse.HouseTemp + - 5

55 Console.WriteLine („Am in“ & Me.mName & _

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

57. Elself mHouse.HouseTemp< mHouse.MAX_TEMP Then

58 vláken. Spánek (200)

59 mHouse.HouseTemp + = 1

60 Console.WriteLine („Am in“ & Me.mName & _

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

62 Jinak

63 Console.WriteLine („Am in“ & Me.mName & _

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

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

66 Konec If

67 Catch tae As ThreadlnterruptedException

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

69 Chytit jako výjimku

70 "Jiné výjimky

71 Konec Zkuste

72 End Sub

73 Koncová třída

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

Procedura Sub Main (řádky 4-7) vytvoří „dům“ s deseti „místnostmi“. Třída House stanoví maximální teplotu 75 stupňů Fahrenheita (asi 24 stupňů Celsia). Řádky 13-28 definují poměrně složitého stavitele domu. Klíčem k pochopení programu jsou řádky 18–27. Řádek 20 vytvoří další objekt místnosti a odkaz na objekt domu je předán konstruktorovi, aby se na něj v případě potřeby mohl odkazovat objekt místnosti. Řádky 21-23 spouští deset proudů pro úpravu teploty v každé místnosti. Třída Room je definována na řádcích 38-73. Reference domu coxpaje uložen v proměnné mHouse v konstruktoru třídy Room (řá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 Sleep.

Sotva by někdo souhlasil s životem v teplotách od 105 stupňů Fahrenheita (40,5 až 24 stupňů Celsia). Co se stalo? Problém souvisí s následujícím řádkem:

Pokud mHouse.HouseTemp< mHouse.MAX_TEMP - 5 Then

A stane se následující: za prvé, teplota je kontrolována průtokem 1. Vidí, že teplota je příliš nízká, a zvýší ji o 5 stupňů. Bohužel, než teplota stoupne, proud 1 se přeruší a řízení se přenese do proudu 2. Stream 2 kontroluje stejnou proměnnou, kterou dosud nebylo změněno tok 1. Tok 2 se tedy také připravuje na zvýšení teploty o 5 stupňů, ale nemá na to čas a také přechází do stavu čekání. Proces pokračuje, dokud není aktivován proud 1 a pokračuje k dalšímu příkazu - zvýšení teploty o 5 stupňů. Nárůst se opakuje, když je aktivováno všech 10 proudů, a obyvatelé domu si budou mít špatně.

Řešení problému: synchronizace

V předchozím programu nastává situace, kdy výstup programu závisí na pořadí provedení vláken. 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, než je přerušeno. 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 příkazů zkombinovaných do atomového bloku nelze plánovačem vláken přerušit, dokud není dokončena. Jakýkoli vícevláknový programovací jazyk má své vlastní způsoby zajištění atomicity. Ve VB .NET je nejjednodušší způsob, jak použít příkaz SyncLock, předat při volání objektovou proměnnou. Proveďte malé změny v proceduře 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 ("Am in" & Me.mName & _

„. Aktuální teplota je“ & mHouse.HouseTemp)

Sám 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) Jinak

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

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

End If Catch tie As ThreadlnterruptedException

„Pasivní čekání přerušila výjimka Catch e As

„Jiné výjimky

Konec Zkuste

Ukončete SyncLock

End Sub

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

Správné použití příkazu SyncLock udržuje vaše vlákno programu v bezpečí. Nadužívání SyncLock má bohužel negativní dopad na výkon. Synchronizace kódu ve vícevláknovém programu několikrát sníží rychlost jeho práce. Synchronizujte pouze nejnutnější kód a uvolněte zámek co nejdříve.

Základní třídy kolekce nejsou ve vícevláknových aplikacích nebezpečné, ale .NET Framework obsahuje verze většiny tříd kolekce bezpečné pro vlákna. V těchto třídách je kód potenciálně nebezpečných metod uzavřen v blocích SyncLock. Ve vícevláknových programech by měly být použity verze tříd kolekcí bezpečné pro vlákna, kdekoli je ohrožena integrita dat.

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

Proměnná podmínka veřejné třídy

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

Soukromý sdílený mOK jako booleovský sdílený

Vlastnost TheConditionVariable () jako logická hodnota

Dostat

Vraťte mOK

Konec Get

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

mOK = hodnota

Ukončete SyncLock

Koncová sada

Koncová vlastnost

Koncová třída

Třída příkazů a monitorů SyncLock

Použití příkazu SyncLock zahrnuje některé jemnosti, které nebyly uvedeny v jednoduchých příkladech výše. Volba synchronizačního objektu tedy hraje velmi důležitou roli. Zkuste spustit předchozí program pomocí příkazu SyncLock (Me) namísto SyncLock (mHouse). Teplota opět stoupne nad práh!

Pamatujte, že příkaz SyncLock se synchronizuje pomocí objekt, předán jako parametr, nikoli fragmentem kódu. Parametr SyncLock funguje jako brána pro přístup k synchronizovanému fragmentu z jiných vláken. Příkaz SyncLock (Me) ve skutečnosti otevírá několik různých „dveří“, což je přesně to, čemu jste se při synchronizaci pokoušeli vyhnout. Morálka:

K ochraně sdílených dat ve vícevláknové aplikaci musí příkaz SyncLock synchronizovat vždy jeden objekt.

Vzhledem k tomu, že synchronizace je spojena s konkrétním objektem, je v některých situacích možné neúmyslně uzamknout další fragmenty. Řekněme, že máte dvě synchronizované metody, první a druhou, a obě metody jsou synchronizovány na objektu bigLock. Když vlákno 1 nejprve zadá metodu a zachytí bigLock, žádné vlákno nebude moci zadat metodu druhou, protože přístup k ní je již omezen na vlákno 1!

Funkčnost příkazu SyncLock lze považovat za podmnožinu funkcí třídy Monitor. Třída Monitor je vysoce přizpůsobitelná a lze ji použít k řešení netriviálních synchronizačních úloh. Příkaz SyncLock je přibližným analogem metod Enter a Exit třídy Moni tor:

Snaž se

Monitor.Enter (theObject) Nakonec

Monitor.Exit (theObject)

Konec Zkuste

U některých standardních operací (zvýšení / snížení proměnné, výměna obsahu dvou proměnných) .NET Framework poskytuje třídu Interlocked, jejíž metody provádějí tyto operace na atomové úrovni. Pomocí třídy Interlocked jsou tyto operace mnohem rychlejší než pomocí příkazu SyncLock.

Blokování

Během synchronizace je zámek nastaven na objekty, nikoli na vlákna, takže při použití odlišný blokovat objekty odlišnýúryvky kódu v programech se někdy vyskytují docela netriviální chyby. Bohužel v mnoha případech je synchronizace na jediném objektu jednoduše nepřijatelná, protože povede k příliš častému blokování vláken.

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

  • Jeden programátor zvládne popadnout nůž a vidličku a začne jíst. Když je plný, odloží večeři stranou a pak je může vzít jiný programátor.
  • Jeden programátor vezme nůž a druhý vezme vidličku. Ani jeden nemůže začít jíst, pokud se ten druhý nevzdá svého spotřebiče.

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 zachycuje objekt 1 a vstupuje do programové části chráněné tímto objektem. Bohužel, aby fungoval, potřebuje přístup ke kódu chráněnému jiným synchronizačním zámkem s jiným synchronizačním objektem. Než ale stihnete zadat fragment, který je synchronizován jiným objektem, vstoupí do něj stream B a tento objekt zachytí. Nyní vlákno A nemůže zadat druhý fragment, vlákno B nemůže zadat první fragment a obě vlákna jsou odsouzena k čekání na neurčito. Žádné vlákno nemůže nadále běžet, protože požadovaný objekt nebude nikdy uvolněn.

Diagnostika mrtvých bodů je komplikována skutečností, že se mohou vyskytnout v relativně vzácných případech. Vše závisí na pořadí, ve kterém jim plánovač přidělí čas CPU. Je možné, že ve většině případů budou synchronizační objekty zachyceny v pořadí bez zablokování.

Následuje implementace právě popsané situace zablokování. Po krátké diskusi o nejzásadnějších bodech si ukážeme, jak identifikovat situaci v zablokování v okně vlákna:

1 Možnost Strict On

2 Importuje systém

3 Module Modulel

4 Dílčí hlavní ()

5 Dim Tom jako nový programátor („Tom“)

6 Dim Bob jako nový programátor („Bob“)

7 Dim aThreadStart jako nový ThreadStart (adresa Tom.Eat)

8 Dim aThread as New Thread (aThreadStart)

9 aThread.Name = "Tom"

10 Dim bThreadStart jako nový 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 Vidlice veřejné třídy

18 Private Shared mForkAvaiTable As Boolean = True

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

20 Soukromý majetek jen pro čtení OwnsUtensil () jako řetězec

21 Získejte

22 Vrátit majitele

23 End Get

24 Koncová vlastnost

25 Public Sub GrabForktByVal a As Programmer)

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

„pokoušet se chytit vidličku.“)

27 Console.WriteLine (Me.OwnsUtensil & "has the fork."). ...

28 Monitor.Enter (Me) "SyncLock (aFork)"

29 Pokud mForkAvailable Then

30 a.HasFork = Pravda

31 vlastník = a.Jméno

32 mForkAvailable = Nepravdivé

33 Console.WriteLine (a.MyName & "just got the fork.waiting")

34 Zkuste

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

Konec Zkuste

35 Konec If

36 Monitor.Exit (Me)

Ukončete SyncLock

37 End Sub

38 Koncová třída

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

40 Private Shared mKnifeAvailable As Boolean = True

41 soukromý sdílený vlastník jako řetězec = „nikdo“

42 Soukromý majetek jen pro čtení OwnsUtensi1 () jako řetězec

43 Získat

44 Vraťte se k vlastníkovi

45 End Get

46 Koncová vlastnost

47 Public Sub GrabKnifetByVal jako programátor)

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

„pokoušet se chytit nůž.“)

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

50 Monitor.Enter (Me) "SyncLock (aKnife)"

51 Pokud je mKnifeAvailable Then

52 mKnifeAvailable = False

53 a.HasKnife = Pravda

54 vlastník = a.Jméno

55 Console.WriteLine (a.MyName & "just got the knife.waiting")

56 Zkuste

Thread.Sleep (100)

Chytit jako výjimku

Console.WriteLine (e.StackTrace)

Konec Zkuste

57 Konec If

58 Monitor.Exit (Me)

59 End Sub

60 Koncová třída

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

62 Soukromé mName As String

63 Private Shared mFork As Fork

64 Soukromý sdílený mNůž jako nůž

65 Soukromý mHasKnife jako booleovský

66 Soukromý mHasFork jako booleovský

67 Shared Sub New ()

68 mFork = nová vidlice ()

69 mKnife = nový nůž ()

70 End Sub

71 Public Sub New (ByVal theName As String)

72 mName = theName

73 Konec Sub

74 Veřejný majetek jen pro čtení MyName () jako řetězec

75 Získejte

76 Návrat mName

77 Konec Získat

78 Koncová vlastnost

79 Veřejný majetek HasKnife () jako booleovský

80 Získejte

81 Návrat mHasKnife

82 End Get

83 Set (ByVal Value As Boolean)

84 mHasKnife = hodnota

85 Koncová sada

86 Koncová vlastnost

87 Veřejný majetek HasFork () jako booleovský

88 Získat

89 Návrat mHasFork

90 Konec Získejte

91 Set (ByVal Value As Boolean)

92 mHasFork = hodnota

93 Koncová sada

94 Koncová vlastnost

95 Public Sub Eat ()

96 Do Do Me.HasKnife And Me.HasFork

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

98 Pokud Rnd ()< 0.5 Then

99 mFork.GrabFork (já)

100 Jinak

101 mKnife.GrabKnife (já)

102 Konec If

103 Smyčka

104 MsgBox (Me.MyName & „can eat!“)

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

106 mFork = nová vidlice ()

107 End Sub

108 Koncová třída

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

Kód pro třídu Fork vypadá zajímavěji (řádky 17-38) (podobná třída Knife je definována v řádcích 39-60). Řádky 18 a 19 určují hodnoty společných polí, podle kterých můžete zjistit, zda je zásuvka aktuálně k dispozici, a pokud ne, kdo ji používá. Vlastnost ReadOnly OwnUtensi1 (řádky 20-24) je určena pro nejjednodušší přenos informací. Centrální pro třídu Fork je metoda GrabFork „grab the fork“, definovaná v řádcích 25-27.

  1. Řádky 26 a 27 jednoduše vytisknou informace o ladění do konzoly. V hlavním kódu metody (řádky 28-36) je přístup k vidlici synchronizován objektemopas mě. Protože náš program používá pouze jednu vidlici, Me sync zajišťuje, že ji nemohou zachytit žádná dvě vlákna současně. Příkaz Slee "p (v bloku začínajícím na řádku 34) simuluje prodlevu mezi chycením vidličky / nože a začátkem jídla. Všimněte si, že příkaz Spánek neodemyká předměty a pouze zrychluje zablokování!
    Nejzajímavější je však kód třídy Programmer (řádky 61-108). Řádky 67–70 definují generický konstruktor, který zajistí, že v programu bude pouze jedna vidlička a nůž. Kód vlastnosti (řádky 74-94) je jednoduchý a nevyžaduje žádný komentář. 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ý proud nezachytí vidličku spolu s nožem. Na linkách 98-102 objekt náhodně uchopí vidličku / nůž pomocí Rnd volání, což je příčinou zablokování. Stává se následující:
    Vlákno, které spouští Eat metodu objektu Thoth, je vyvoláno a vstupuje do smyčky. Popadne nůž a přejde do stavu čekání.
  2. Je vyvoláno vlákno provádějící Bobovu Eat metodu a vstupuje do smyčky. Nemůže chytit nůž, ale uchopí vidličku a přejde do stavu čekání.
  3. Vlákno, které spouští Eat metodu objektu Thoth, je vyvoláno a vstupuje do smyčky. Snaží se chytit vidličku, ale Bob už vidlici chytil; vlákno přejde do stavu čekání.
  4. Je vyvoláno vlákno provádějící Bobovu Eat metodu a vstupuje do smyčky. Snaží se uchopit nůž, ale nůž je již zajat předmětem Thoth; vlákno přejde do stavu čekání.

To vše pokračuje donekonečna - potýkáme se s typickou situací zablokování (zkuste spustit program a uvidíte, že nikdo není schopen takto jíst).
V okně vláken můžete také zkontrolovat, zda nedošlo k zablokování. Spusťte program a přerušte jej pomocí kláves Ctrl + Break. Zahrňte do výřezu proměnnou Me a otevřete okno streamů. Výsledek vypadá jako na obrázku. 10.7. Z obrázku můžete vidět, že Bobova nit chytila ​​nůž, ale nemá vidličku. Klepněte pravým tlačítkem v okně Vlákna na řádku Tot a v místní nabídce vyberte příkaz Přepnout na vlákno. Výřez ukazuje, že proud Thoth má vidličku, ale žádný nůž. Samozřejmě to není stoprocentní důkaz, ale takové chování přinejmenším vyvolává podezření, že něco není v pořádku.
Pokud možnost se synchronizací jedním objektem (jako v programu se zvýšením -teploty v domě) není možná, abyste zabránili vzájemným zámkům, můžete synchronizační objekty očíslovat a vždy je zachytit v konstantním pořadí. Pokračujme v analogii jídelního programátora: pokud vlákno vždy vezme nejprve nůž a poté vidličku, nebudou problémy se zablokováním. První proud, který chytne nůž, bude moci normálně jíst. Přeloženo do jazyka programových proudů, to znamená, že zachycení objektu 2 je možné pouze tehdy, pokud je objekt 1 poprvé zachycen.

Rýže. 10.7. Analýza zablokování v okně vlákna

Pokud tedy odstraníme volání Rnd na řádku 98 a nahradíme jej úryvkem

mFork.GrabFork (já)

mKnife.GrabKnife (já)

mrtvý bod zmizí!

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

Ve vícevláknových aplikacích často dochází k situaci, kdy vlákna nejen fungují se sdílenými daty, ale také čekají, až se objeví (to znamená, že vlákno 1 musí vytvořit data, než ho vlákno 2 může použít). Jelikož jsou data sdílena, je třeba k nim synchronizovat přístup. Je také nutné zajistit prostředky pro upozornění čekajících vláken na vzhled připravených dat.

Tato situace se obvykle nazývá problém dodavatele / spotřebitele. Vlákno se pokouší o přístup k datům, která ještě neexistují, takže musí přenést řízení do jiného vlákna, které vytvoří požadovaná data. Problém je vyřešen následujícím kódem:

  • Vlákno 1 (spotřebitel) se probudí, zadá synchronizovanou metodu, vyhledá data, nenajde je a přejde do stavu čekání. Předběžněfyzicky musí blokování odstranit, aby nepřekáželo v práci dodávajícího vlákna.
  • Vlákno 2 (poskytovatel) zadává synchronizovanou metodu uvolněnou vláknem 1, vytváří data pro stream 1 a nějakým způsobem upozorní stream 1 na přítomnost dat. Poté uvolní zámek, aby vlákno 1 mohlo zpracovávat nová data.

Nepokoušejte se tento problém vyřešit neustálým vyvoláním vlákna 1 a kontrolou stavu proměnné podmínky, jejíž hodnota je> nastavena vláknem 2. Toto rozhodnutí vážně ovlivní výkon vašeho programu, protože ve většině případů vlákno 1 být vyvolán bez důvodu; a vlákno 2 bude čekat tak často, že mu dojde čas na vytvoření dat.

Vztahy mezi poskytovatelem a spotřebitelem jsou velmi běžné, proto jsou pro takové situace ve vícevláknových knihovnách tříd programování vytvořeny speciální primitiva. V NET se těmto primitivům říká Wait a Pulse-PulseAl 1 a jsou součástí třídy Monitor. Obrázek 10.8 ilustruje situaci, kterou se chystáme naprogramovat. Program organizuje tři fronty vláken: fronta čekání, blokovací fronta a fronta spuštění. Plánovač vláken nepřiděluje čas procesoru vláknům, která jsou ve frontě čekajících. Aby se vláknu přidělil čas, musí se přesunout do fronty spuštění. Výsledkem je, že práce aplikace je organizována mnohem efektivněji než s obvyklým dotazováním podmíněné proměnné.

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

"Vstup do synchronizovaného bloku následujícího typu."

Zatímco žádná data

Přejděte do fronty na čekání

Smyčka

Pokud existují data, zpracujte je.

Opusťte synchronizovaný blok

Bezprostředně po provedení příkazu Čekat se vlákno pozastaví, zámek se uvolní a vlákno vstoupí do fronty čekajících. Když je zámek uvolněn, vlákno ve frontě spuštění se může spustit. V průběhu času vytvoří jedno nebo více blokovaných vláken data nezbytná pro provoz vlákna, které je ve frontě čekajících. Jelikož se validace dat provádí ve smyčce, k přechodu na používání dat (po smyčce) dochází 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řejděte do fronty na čekání

Jinak produkovat data

Když jsou data připravena, zavolejte Pulse-PulseAll.

přesunout jedno nebo více vláken z blokovací fronty do fronty spouštění. Opusťte synchronizovaný blok (a vraťte se do fronty spuštění)

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číukazuje se, že dítě musí čekat na příchod nové částky. Softwarová implementace tohoto modelu vypadá takto:

1 Možnost Strict On

2 Importuje systém

3 Module Modulel

4 Dílčí hlavní ()

5 Dim the Family as New Family ()

6 theFamily.StartltsLife ()

7 End Sub

8 Koncový fjodule

9

10 Rodina veřejné třídy

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

12 Soukromý mTýden jako celé číslo = 1

13 Public Sub StartltsLife ()

14 Dim aThreadStart as New ThreadStarUAddressOf Me.Produce)

15 Dim bThreadStart jako nový ThreadStarUAddressOf Me.Consume)

16 Dim aThread as New Thread (aThreadStart)

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

18 aThread.Name = "Produkovat"

19 aThread.Start ()

20 bThread.Name = "Konzumovat"

21 bVlákno. Start ()

22 Konec Sub

23 Veřejný majetek TheWeek () jako celé číslo

24 Získejte

25 Zpětný týden

26 End Get

27 Set (ByVal Value As Integer)

28 týdnů - hodnota

29 Koncová sada

30 Koncová vlastnost

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

32 Získat

33 Vrácení peněz

34 End Get

35 Set (ByVal Value As Integer)

36 mPeníze = hodnota

37 Koncová sada

38 Koncová vlastnost

39 Veřejná subprodukce ()

40 vláken. Spánek (500)

41 Do

42 Monitor. Zadejte (já)

43 Do While Me.OurMoney> 0

44 Monitor. Počkejte (já)

45 Smyčka

46 Me. Naše peníze = 1000

47 Monitor.PulseAll (já)

48 Monitor.Exit (Me)

49 Smyčka

50 End Sub

51 Public Sub Consume ()

52 MsgBox („Jsem ve vlákně konzumovat“)

53 Do

54 Monitor.Enter (Me)

55 Do While Me.OurMoney = 0

56 Monitor. Počkejte (já)

57 Smyčka

58 Console.WriteLine („Milý rodič, právě jsem strávil všechny tvé“ & _

peníze za týden "& TheWeek)

59 Týden + = 1

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

61 Me.OurMoney = 0

62 Monitor.PulseAll (Me)

63 Monitor.Exit (Me)

64 Smyčka

65 End Sub

66 Koncová třída

Metoda StartltsLife (řádky 13-22) se připravuje na spuštění streamů Produce a Consume. To nejdůležitější se děje ve streamech Produce (řádky 39-50) a Consume (řádky 51-65). Procedura Sub Produce kontroluje dostupnost peněz, a pokud jsou peníze, přejde do čekací fronty. V opačném případě rodič generuje peníze (řádek 46) a upozorní objekty ve frontě čekajících na změnu situace. Všimněte si, že volání Pulse-Pulse All se projeví pouze tehdy, když je zámek uvolněn příkazem Monitor.Exit. Naopak postup Sub Consume kontroluje dostupnost peněz, a pokud peníze nejsou, upozorní na to nastávajícího rodiče. Řádek 60 jednoduše ukončí program po 21 podmíněných letech; volání systému. Environment.Exit (0) je analog .NET příkazu End (příkaz End je také podporován, ale na rozdíl od System. Environment. Exit nevrací kód ukončení operačního systému).

Vlákna, která jsou zařazena do čekací fronty, musí být uvolněna jinými částmi vašeho programu. Z tohoto důvodu dáváme přednost použití PulseAll přes 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ě můžete stejně dobře volat PulseAll.

Vícevláknové zpracování v grafických programech

Naše diskuse o multithreadingu v aplikacích GUI začíná příkladem, který vysvětluje, k čemu multithreading v aplikacích GUI slouží. Vytvořte formulář pomocí dvou tlačítek Start (btnStart) a Cancel (btnCancel), jak ukazuje obr. 10.9. Kliknutím na tlačítko Start vygenerujete třídu, 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 pro efektivnější vytváření dlouhých řetězců.

Krok 1

Vlákno 1 si všimne, že pro něj neexistují žádná data. Zavolá Wait, uvolní zámek a přejde do fronty čekání.



Krok 2

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

Krok 3

Řekněme, že vlákno 3 zadá synchronizovaný blok, vytvoří data a zavolá Pulse-Pulse All.

Ihned poté, co opustí blok a uvolní zámek, se vlákno 1 přesune do fronty spuštění. Pokud vlákno 3 volá Pluse, do fronty spuštění vstoupí pouze jedenvlákno, při volání Pluse All přejdou všechna vlákna do fronty spuštění.



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

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

Importuje System.Text

Veřejná třída Náhodné znaky

Soukromé m_Data jako StringBuilder

Soukromá mjength, m_count As Integer

Public Sub New (ByVal n As Integer)

m_Délka = n -1

m_Data = Nový StringBuilder (m_length) MakeString ()

End Sub

Private Sub MakeString ()

Dim i As Integer

Dim myRnd As New Random ()

Pro i = 0 až m_délka

„Vygenerujte náhodné číslo mezi 65 a 90,

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

"a připojte k objektu StringBuilder

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

další

End Sub

Veřejný dílčí počáteční počet ()

GetEes ()

End Sub

Soukromé Sub GetEes ()

Dim i As Integer

Pro i = 0 až m_délka

Pokud m_Data.Chars (i) = CChar ("E") Pak

m_count + = 1

End If Next

m_CountDone = Pravda

End Sub

Veřejné pouze pro čtení

Vlastnost GetCount () jako celé číslo

Pokud ne (m_CountDone) Pak

Vrátit m_count

Konec If

End Získejte End Property

Veřejné pouze pro čtení

Property IsDone () As Boolean Get

Vrátit se

m_CountDone

Konec Get

Koncová vlastnost

Koncová třída

Ke dvěma tlačítkům ve formuláři je přidružen velmi jednoduchý kód. 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 sender As System.Object.

ByVal e As System.EventArgs) Zpracovává btnSTart.Click

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

RC.StartCount ()

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

End Sub

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

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

ByVal e As System.EventArgs) Zpracovává btnCancel.Click

MsgBox („Count Interrupted!“)

End Sub

Když je program spuštěn a je stisknuto tlačítko Start, ukáže se, že tlačítko Storno nereaguje na vstup uživatele, protože souvislá smyčka brání tlačítku zpracovávat událost, kterou obdrží. To je v moderních programech nepřijatelné!

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

Application.DoEvents ()

V našem případě to rozhodně není žádoucí - kdo chce zpomalit program deseti miliony volání DoEvents! Pokud místo toho přidělíte smyčku samostatnému vláknu, operační systém přepne mezi vlákny a tlačítko Storno zůstane funkční. Implementace se samostatným vláknem je uvedena níže. Abychom jasně ukázali, že tlačítko Storno funguje, když na něj klikneme, jednoduše program ukončíme.

Další krok: Tlačítko Zobrazit počet

Řekněme, že jste se rozhodli ukázat svou kreativní představivost a dát formě vzhled zobrazený na obr. 10.9. Poznámka: tlačítko Zobrazit počet ještě není k dispozici.

Rýže. 10.10. Uzamčený formulář tlačítka

Očekává se, že samostatné vlákno provede počítání a odemkne nedostupné tlačítko. To lze samozřejmě provést; navíc takový úkol vzniká poměrně často. Bohužel nebudete moci jednat nejzjevnějším způsobem - propojte sekundární vlákno s vláknem GUI tak, že v konstruktoru ponecháte odkaz na tlačítko ShowCount, nebo dokonce použijete standardního delegáta. Jinými slovy, nikdy nepoužívejte níže uvedenou možnost (základní chybnýřádky jsou tučně).

Veřejná třída Náhodné znaky

Soukromý m_0ata jako StringBuilder

Soukromý m_CountDone jako booleovský

Soukromá mjength. m_count jako celé číslo

Soukromé m_Button jako Windows.Forms.Button

Public Sub New (ByVa1 n As Integer, _

ByVal b jako Windows.Forms.Button)

m_length = n - 1

m_Data = Nový StringBuilder (mJength)

m_Button = b MakeString ()

End 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ší

End Sub

Veřejný dílčí počáteční počet ()

GetEes ()

End Sub

Soukromé Sub GetEes ()

Dim I As Integer

For I = 0 To mjength

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

m_count + = 1

End If Next

m_CountDone = Pravda

m_Button.Enabled = True

End Sub

Veřejné pouze pro čtení

Vlastnost GetCount () jako celé číslo

Dostat

Pokud ne (m_CountDone) Pak

Hoďte novou výjimku („Počet ještě není dokončen“)

Vrátit m_count

Konec If

Konec Get

Koncová vlastnost

Veřejné vlastnictví jen pro čtení je provedeno () jako logická hodnota

Dostat

Vrátit m_CountDone

Konec Get

Koncová vlastnost

Koncová třída

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

  • Interakce sekundárního vlákna s vláknem vytvářejícím GUI nelze organizovat zřejmé prostředek.
  • Nikdy neupravujte prvky v grafických programech z jiných programových proudů. Všechny změny by měly nastat pouze ve vlákně, které vytvořilo GUI.

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

Nepodaří se také zorganizovat interakci objektů pomocí událostí. Pracovník události 06 běží na stejném vlákně, kterému byl RaiseEvent nazván, takže vám události nepomohou.

Přesto zdravý rozum říká, že grafické aplikace musí mít prostředky na úpravu prvků z jiného vlákna. V NET Framework existuje způsob, jak bezpečně zavolat metody GUI aplikací z jiného vlákna. K tomuto účelu se používá speciální typ delegáta Method Invoker z oboru názvů System.Windows. Formuláře. Následující úryvek ukazuje novou verzi metody GetEes (změněné řádky tučně):

Soukromé Sub GetEes ()

Dim I As Integer

Pro I = 0 až m_délka

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

m_count + = 1

End If Next

m_CountDone = Skutečný pokus

Dim mylnvoker as New Methodlnvoker (AddressOf UpDateButton)

myInvoker.Invoke () Catch e As ThreadlnterruptedException

"Selhání

Konec Zkuste

End Sub

Public Sub UpDateButton ()

m_Button.Enabled = True

End Sub

Volání mezi vlákny na tlačítko se neprovádějí přímo, ale prostřednictvím Method Invoker. Rozhraní .NET Framework zaručuje, že tato možnost je bezpečná pro vlákna.

Proč je tolik problémů s vícevláknovým programováním?

Nyní, když už trochu rozumíte multithreadingu a potenciálním problémům s ním spojeným, jsme se rozhodli, že by bylo vhodné zodpovědět otázku v záhlaví tohoto pododdílu na konci této kapitoly.

Jedním z důvodů je, že multithreading je nelineární proces a jsme zvyklí na lineární programovací model. Zpočátku je těžké si zvyknout na samotnou myšlenku, že provádění programu lze náhodně přerušit a ovládání bude přeneseno do jiného kódu.

Existuje však ještě jeden, zásadnější důvod: v dnešní době programátoři příliš zřídka programují v assembleru, nebo se alespoň dívají na rozebraný výstup kompilátoru. Jinak by si mnohem snáze zvykli na myšlenku, že desítky montážních pokynů mohou odpovídat jednomu příkazu jazyka na vysoké úrovni (například VB .NET). Vlákno lze přerušit po kterémkoli z těchto pokynů, a proto uprostřed příkazu na vysoké úrovni.

Ale to není vše: moderní kompilátory optimalizují výkon programu a počítačový hardware může zasahovat do správy paměti. V důsledku toho může překladač nebo hardware změnit pořadí příkazů uvedených ve zdrojovém kódu programu bez vašeho vědomí [ Mnoho kompilátorů optimalizuje cyklické kopírování polí 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 místo kopírování jednotlivých prvků mnohokrát naplnit jedinou operací kopírování!].

Naštěstí vám tato vysvětlení pomohou lépe pochopit, proč vícevláknové programování způsobuje tolik problémů - nebo alespoň menší překvapení nad podivným chováním vašich vícevláknových programů!

Příklad vytvoření jednoduché vícevláknové aplikace.

Narodil se kvůli mnoha otázkám ohledně vytváření vícevláknových aplikací v Delphi.

Účelem tohoto příkladu je ukázat, jak správně sestavit vícevláknovou aplikaci s odstraněním dlouhodobé práce v samostatném vlákně. A jak v takové aplikaci zajistit interakci hlavního vlákna s pracovníkem pro přenos dat z formuláře (vizuální komponenty) do streamu a naopak.

Příklad netvrdí, že je úplný, ukazuje pouze nejjednodušší způsoby interakce mezi vlákny. Umožnění uživateli „rychle oslepit“ (kdo by věděl, jak moc to nesnáším) správně fungující vícevláknové aplikaci.
Vše v něm je podrobně komentováno (podle mě), ale pokud máte nějaké dotazy, ptejte se.
Ale ještě jednou vás varuji: Streamování není snadné... Pokud nemáte tušení, jak to všechno funguje, pak existuje obrovské nebezpečí, že vám často všechno bude dobře fungovat a někdy se program bude chovat více než divně. Chování nesprávně napsaného vícevláknového programu je vysoce závislé na velkém počtu faktorů, které někdy nelze během ladění reprodukovat.

Takže příklad. Pro pohodlí jsem vložil kód a připojil archiv k modulu a kódu formuláře

jednotka ExThreadForm;

použití
Windows, Zprávy, SysUtils, Varianty, Třídy, Grafika, Ovládací prvky, Formuláře,
Dialogy, StdCtrls;

// konstanty používané při přenosu dat z datového proudu do formuláře pomocí
// odesílání okenních zpráv
konst
WM_USER_SendMessageMetod = WM_USER + 10;
WM_USER_PostMessageMetod = WM_USER + 11;

typ
// popis třídy vlákna, potomek tThread
tMyThread = třída (tThread)
soukromé
SyncDataN: Integer;
SyncDataS: String;
procedura SyncMetod1;
chráněný
postup Provést; přepsat;
veřejnost
Param1: String;
Param2: Integer;
Param3: Boolean;
Zastaveno: Boolean;
LastRandom: Integer;
IterationNo: Integer;
ResultList: tStringList;

Vytvořit konstruktor (aParam1: String);
destruktor Destroy; přepsat;
konec;

// popis třídy formuláře pomocí streamu
TForm1 = třída (TForm)
Label1: TLabel;
Memo1: TMemo;
btnStart: TButton;
btnStop: TButton;
Edit1: TEdit;
Edit2: TEdit;
CheckBox1: TCheckBox;
Label2: TLabel;
Label3: TLabel;
Label4: TLabel;
postup btnStartClick (Odesílatel: TObject);
postup btnStopClick (Odesílatel: TObject);
soukromé
(Soukromá prohlášení)
MyThread: tMyThread;
procedura EventMyThreadOnTerminate (Odesílatel: tObject);
postup EventOnSendMessageMetod (var Msg: TMessage); zpráva WM_USER_SendMessageMetod;
procedura EventOnPostMessageMetod (var Msg: TMessage); zpráva WM_USER_PostMessageMetod;

Veřejnost
(Veřejná prohlášení)
konec;

var
Form1: TForm1;

{
Zastaveno - Ukazuje přenos dat z formuláře do streamu.
Další synchronizace není nutná, protože je jednoduchá
jednoslovný typ a je napsán pouze jedním vláknem.
}

postup TForm1.btnStartClick (Odesílatel: TObject);
začít
Randomize (); // zajištění náhodnosti v sekvenci pomocí Random () - nemá s tokem nic společného

// Vytvořte instanci objektu streamu a předejte mu vstupní parametr
{
POZORNOST!
Konstruktor streamu je napsán takovým způsobem, že se vytvoří proud
pozastaveno, protože umožňuje:
1. Ovládejte okamžik jeho spuštění. To je téměř vždy pohodlnější, protože
umožňuje nastavit stream ještě před spuštěním, předat jej vstupu
parametry atd.
2. Protože odkaz na vytvořený objekt bude uložen do pole formuláře, poté
po samodestrukci vlákna (viz níže), které když vlákno běží
může dojít kdykoli, tento odkaz bude neplatný.
}
MyThread: = tMyThread.Create (Form1.Edit1.Text);

// Nicméně, protože vlákno bylo vytvořeno pozastavené, pak na jakékoli chyby
// během jeho inicializace (před spuštěním) jej musíme zničit sami
// k tomu, co používáme try / kromě block
Snaž se

// Přiřazení obsluhy ukončení vlákna, ve které budeme přijímat
// výsledky práce streamu, a „přepsat“ na něj odkaz
MyThread.OnTerminate: = EventMyThreadOnTerminate;

// Protože výsledky budou shromažďovány v OnTerminate, tj. před sebezničením
// stream, pak odstraníme starosti s jeho zničením
MyThread.FreeOnTerminate: = True;

// Příklad předávání vstupních parametrů přes pole objektu streamu v daném bodě
// vytvořte instanci, pokud již není spuštěna.
// Osobně to preferuji pomocí parametrů přepsaného
// konstruktor (tMyThread.Create)
MyThread.Param2: = StrToInt (Form1.Edit2.Text);

MyThread.Stopped: = False; // taky druh parametru, ale mění se
// doba běhu vlákna
až na
// jelikož vlákno ještě nezačalo a nebude se moci samo zničit, zničíme ho „ručně“
FreeAndNil (MyThread);
// a poté nechejte výjimku zpracovat jako obvykle
vyzdvihnout;
konec;

// Protože byl objekt vlákna úspěšně vytvořen a nakonfigurován, je na čase jej spustit
MyThread.Resume;

ShowMessage („Stream spuštěn“);
konec;

postup TForm1.btnStopClick (Odesílatel: TObject);
začít
// Pokud instance vlákna stále existuje, požádejte ji o zastavení
// A přesně "zeptejte se". V zásadě také můžeme „nutit“, ale bude
// extrémně nouzová možnost vyžadující jasné pochopení toho všeho
// streamovací kuchyně. Proto se zde neuvažuje.
pokud Assigned (MyThread) then
MyThread.Stopped: = Pravda
jiný
ShowMessage („Vlákno není spuštěno!“);
konec;

postup TForm1.EventOnSendMessageMetod (var Msg: TMessage);
začít
// metoda pro zpracování synchronní zprávy
// ve WParam adresa objektu tMyThread, v LParam aktuální hodnota LastRandom vlákna
s tMyThread (Msg.WParam) začít
Form1.Label3.Caption: = Formát ("% d% d% d",);
konec;
konec;

postup TForm1.EventOnPostMessageMetod (var Msg: TMessage);
začít
// metoda pro zpracování asynchronní zprávy
// ve WParam aktuální hodnota IterationNo, v LParam aktuální hodnota streamu LastRandom
Form1.Label4.Caption: = Formát ("% d% d",);
konec;

procedura TForm1.EventMyThreadOnTerminate (Odesílatel: tObject);
začít
// DŮLEŽITÉ!
// Metoda pro zpracování události OnTerminate je vždy volána v kontextu main
// vlákno - to je zaručeno implementací tThread. Proto v něm můžete svobodně
// použijte jakékoli vlastnosti a metody jakýchkoli objektů

// Jen pro případ, ujistěte se, že instance objektu stále existuje
pokud není přiřazeno (MyThread), pak Konec; // pokud tam není, pak není co dělat

// získejte výsledky práce vlákna instance objektu vlákna
Form1.Memo1.Lines.Add (Format ("Stream skončil výsledkem% d",));
Form1.Memo1.Lines.AddStrings ((Odesílatel jako tMyThread) .ResultList);

// Zničte odkaz na instanci objektu streamu.
// Protože se naše vlákno samo zničí (FreeOnTerminate: = True)
// pak po dokončení obslužné rutiny OnTerminate bude instance objektu streamu
// zničeno (zdarma) a všechny odkazy na něj budou neplatné.
// Abyste na takový odkaz náhodou nenarazili, přepište MyThread
// Ještě jednou poznamenám - nezničíme objekt, ale pouze přepíšeme odkaz. Objekt
// zničit sebe!
MyThread: = Nil;
konec;

konstruktor tMyThread.Create (aParam1: String);
začít
// Vytvořte instanci SUSPENDED streamu (při vytváření instance viz komentář)
zděděné Create (True);

// Vytvoření interních objektů (je -li to nutné)
ResultList: = tStringList.Create;

// Získejte počáteční data.

// Zkopírujte vstupní data předaná parametrem
Param1: = aParam1;

// Příklad přijímání vstupních dat z komponent VCL v konstruktoru objektu streamu
// To je v tomto případě přijatelné, protože konstruktor je volán v kontextu
// hlavní vlákno. Proto lze ke komponentám VCL přistupovat zde.
// Ale tohle se mi nelíbí, protože si myslím, že je špatné, když vlákno něco ví
// o nějaké formě tam. Ale co nemůžete udělat pro ukázku.
Param3: = Form1.CheckBox1.Checked;
konec;

destruktor tMyThread.Destroy;
začít
// zničení vnitřních předmětů
FreeAndNil (ResultList);
// zničit základní tThread
zděděno;
konec;

procedura tMyThread.Execute;
var
t: kardinál;
s: Řetězec;
začít
IterationNo: = 0; // počítadlo výsledků (číslo cyklu)

// V mém příkladu je tělem vlákna smyčka, která končí
// nebo externím „požadavkem“ na ukončení předaným prostřednictvím parametru proměnné Zastaveno,
// buď pouhým provedením 5 smyček
// Je pro mě příjemnější psát to „věčnou“ smyčkou.

Zatímco True začíná

Inc (IterationNo); // číslo dalšího cyklu

LastRandom: = Náhodné (1000); // číslo klíče - k předvedení přenosu parametrů ze streamu do formuláře

T: = Náhodné (5) +1; // doba, na kterou usneme, pokud nebudeme dokončeni

// Hloupá práce (v závislosti na vstupním parametru)
pokud ne Param3, pak
Inc (Param2)
jiný
Dec (Param2);

// Vytvořte mezivýsledek
s: = Formát ("% s% 5d% s% d% d",
);

// Přidání přechodného výsledku do seznamu výsledků
ResultList.Add (s);

//// Příklady předání přechodného výsledku do formuláře

//// Předávání synchronizovanou metodou - klasický způsob
//// Nevýhody:
//// - synchronizovaná metoda je obvykle metodou třídy streamů (pro přístup
//// do polí objektu streamu), ale pro přístup do polí formuláře musí
//// „vím“ o něm a jeho polích (objektech), což se s ním obvykle příliš nedaří
//// hledisko organizace programu.
//// - aktuální vlákno bude pozastaveno, dokud nebude provádění dokončeno
//// synchronizovaná metoda.

//// Výhody:
//// - standardní a univerzální
//// - v synchronizované metodě můžete použít
//// všechna pole objektu streamu.
// nejprve, v případě potřeby, musíte uložit přenesená data do
// speciální pole objektu objektu.
SyncDataN: = IterationNo;
SyncDataS: = "Sync" + s;
// a poté poskytněte synchronizované volání metody
Synchronizovat (SyncMetod1);

//// Odesílání prostřednictvím synchronního odesílání zpráv (SendMessage)
//// v tomto případě mohou být data předávána prostřednictvím parametrů zprávy (LastRandom),
//// a přes pole objektu předáním adresy instance v parametru zprávy
//// objektu streamu - Integer (Self).
//// Nevýhody:
//// - vlákno musí znát popisovač okna formuláře
//// - stejně jako u Synchronize bude aktuální vlákno pozastaveno do
//// dokončení zpracování zprávy hlavním vláknem
//// - vyžaduje značné množství času CPU pro každé volání
//// (pro přepínání vláken), proto je velmi časté volání nežádoucí
//// Výhody:
//// - jako u Synchronize můžete při zpracování zprávy použít
//// všechna pole objektu streamu (pokud byla samozřejmě předána jeho adresa)


//// založ vlákno.
SendMessage (Form1.Handle, WM_USER_SendMessageMetod, Integer (Self), LastRandom);

//// Přenos pomocí asynchronního odesílání zpráv (PostMessage)
//// Protože v tomto případě, v době, kdy je zpráva přijata hlavním vláknem,
//// odesílací stream již mohl být dokončen, přičemž byla předána adresa instance
//// objekt streamu je neplatný!
//// Nevýhody:
//// - vlákno musí znát úchyt okna formuláře;
//// - kvůli asynchronii je přenos dat možný pouze pomocí parametrů
//// zprávy, což výrazně komplikuje přenos dat, která mají velikost
//// více než dvě strojová slova. Je vhodné použít pro předávání Integeru atd.
//// Výhody:
//// - na rozdíl od předchozích metod aktuální vlákno NEBUDE
//// je pozastaveno a okamžitě obnoví provádění
//// - na rozdíl od synchronizovaného hovoru, obsluha zpráv
//// je metoda formuláře, která musí mít znalosti o objektu streamu,
//// nebo o streamu nevíte vůbec nic, pokud jsou přenášena pouze data
//// prostřednictvím parametrů zprávy. To znamená, že vlákno nemusí vědět nic o formuláři.
//// obecně - pouze její Handle, kterou lze předat jako parametr dříve
//// založ vlákno.
PostMessage (Form1.Handle, WM_USER_PostMessageMetod, IterationNo, LastRandom);

//// Zkontrolujte možné dokončení

// Kontrola dokončení podle parametru
pokud je zastaven, pak Break;

// Příležitostně zkontrolujte dokončení
pokud IterationNo> = 10, pak Break;

Spánek (t * 1000); // Usni na t sekund
konec;
konec;

procedura tMyThread.SyncMetod1;
začít
// tato metoda se nazývá metodou Synchronize.
// Tedy navzdory skutečnosti, že se jedná o metodu vlákna tMyThread,
// běží v kontextu hlavního vlákna aplikace.
// Proto může dělat cokoli, dobře, nebo téměř všechno :)
// Pamatujte ale, že nemá cenu se tu dlouho „motat“

// Předané parametry můžeme extrahovat ze speciálních polí, kde je máme
// uloženo před voláním.
Form1.Label1.Caption: = SyncDataS;

// buď z jiných polí objektu streamu, například podle jeho aktuálního stavu
Form1.Label2.Caption: = Formát ("% d% d",);
konec;

Obecně tomu příkladu předcházela moje následující úvaha k tématu ...

Za prvé:
NEJDŮLEŽITĚJŠÍ pravidlo vícevláknového programování v Delphi je:
V kontextu jiného než hlavního vlákna nemůžete přistupovat k vlastnostem a metodám formulářů a vlastně ke všem komponentám, které „rostou“ z tWinControl.

To znamená (poněkud zjednodušeně), že ani v metodě Execute zděděné z TThread, ani v jiných metodách / procedurách / funkcích volaných z Execute, je to zakázáno přímý přístup k jakýmkoli vlastnostem a metodám vizuálních komponent.

Jak to udělat správně.
Neexistují jednotné recepty. Přesněji řečeno, existuje tolik a různých možností, že si v závislosti na konkrétním případě musíte vybrat. Proto odkazují na článek. Když si to programátor přečte a porozumí, bude schopen porozumět a jak to v konkrétním případě nejlépe udělat.

Stručně řečeno na prstech:

Vícevláknová aplikace se nejčastěji stává buď tehdy, když je nutné provést nějaký druh dlouhodobé práce, nebo když je možné současně provádět několik věcí, které nezatěžují procesor.

V prvním případě vede implementace práce uvnitř hlavního vlákna k „zpomalení“ uživatelského rozhraní - zatímco se práce provádí, smyčka zpráv se neprovádí. Výsledkem je, že program nereaguje na akce uživatele a formulář se nevykreslí, například poté, co jej uživatel přesune.

V druhém případě, kdy práce zahrnuje aktivní výměnu s vnějším světem, pak během vynucených „prostojů“. Během čekání na příjem / odesílání dat můžete paralelně dělat něco jiného, ​​například znovu odesílat / přijímat data.

Existují i ​​jiné případy, ale méně často. Na tom však nezáleží. Teď o tom nejde.

A teď, jak je to všechno napsané. Přirozeně se uvažuje o určitém nejčastějším, poněkud zobecněném případě. Tak.

Práce provedená v samostatném vlákně má v obecném případě čtyři entity (nevím, jak to přesněji nazvat):
1. Počáteční data
2. Vlastně samotná práce (může záviset na počátečních datech)
3. Mezilehlá data (například informace o aktuálním stavu provádění práce)
4. Výstupní data (výsledek)

Ke čtení a zobrazení většiny dat se nejčastěji používají vizuální komponenty. Ale, jak bylo uvedeno výše, nemůžete přímo získat přístup k vizuálním komponentám ze streamu. Jak být?
Vývojáři Delphi navrhují použít metodu Synchronize třídy TThread. Zde nebudu popisovat, jak jej používat - k tomu existuje výše zmíněný článek. Dovolte mi jen říci, že jeho aplikace, byť ta správná, není vždy odůvodněná. Existují dva problémy:

Za prvé, tělo metody nazvané přes Synchronize je vždy spuštěno v kontextu hlavního vlákna, a proto, zatímco se provádí, opět není spuštěna smyčka zpráv okna. Proto musí být spuštěn rychle, jinak budeme mít všechny stejné problémy jako s implementací s jedním vláknem. V ideálním případě by metoda volaná prostřednictvím Synchronizovat měla být obecně používána pouze pro přístup k vlastnostem a metodám vizuálních objektů.

Za druhé, provedení metody prostřednictvím Synchronize je „drahé“ potěšení kvůli potřebě dvou přepínačů mezi vlákny.

Oba problémy jsou navíc propojené a způsobují rozpor: na jedné straně je k vyřešení prvního nutné „rozemlet“ metody vyvolávané pomocí Synchronize a na straně druhé je pak často nutné volat, přičemž ztrácí drahocenné prostředky procesoru.

Proto, jako vždy, je nutné přistupovat rozumně a v různých případech použít různé způsoby interakce toku s vnějším světem:

Počáteční data
Všechna data, která jsou přenášena do streamu a která se během provozu nemění, musí být přenesena ještě před spuštěním, tj. při vytváření streamu. Abyste je mohli použít v těle vlákna, musíte si je vytvořit lokálně (obvykle v polích potomka TThread).
Pokud existují počáteční data, která se mohou během běhu vlákna měnit, pak k takovým datům je nutné přistupovat buď prostřednictvím synchronizovaných metod (metody nazývané prostřednictvím Synchronize), nebo prostřednictvím polí objektu vlákna (potomek TThread). To druhé vyžaduje určitou opatrnost.

Mezilehlá a výstupní data
Zde opět existuje několik způsobů (v pořadí podle mých preferencí):
- Metoda asynchronního odesílání zpráv do hlavního okna aplikace.
Obvykle se používá k odesílání zpráv o průběhu procesu do hlavního okna aplikace s přenosem malého množství dat (například procento dokončení)
- Způsob synchronního odesílání zpráv do hlavního okna aplikace.
Obvykle se používá ke stejným účelům jako asynchronní odesílání, ale umožňuje přenášet větší množství dat bez vytváření samostatné kopie.
- Synchronizované metody, pokud je to možné, kombinující přenos co největšího počtu dat do jedné metody.
Lze také použít k načtení dat z formuláře.
- Prostřednictvím polí objektu streamu poskytující vzájemně se vylučující přístup.
Více podrobností najdete v článku.

Eh. Krátce to nevyšlo