Na aké účely sa používajú viacvláknové systémy. Osem jednoduchých pravidiel pre vývoj viacvláknových aplikácií

Ktorá téma vyvoláva najviac otázok a ťažkostí pre začiatočníkov? Keď som sa na to opýtal svojho učiteľa a programátora Java Alexandra Pryakhina, okamžite odpovedal: „Viacvláknové“. Ďakujem mu za nápad a pomoc pri príprave tohto článku!

Nahliadneme do vnútorného sveta aplikácie a jej procesov, zistíme, čo je podstatou viacvláknového vlákna, kedy je užitočné a ako ho implementovať - ​​ako príklad použijeme Javu. Ak sa učíte iný jazyk OOP, nebojte sa: základné princípy sú rovnaké.

O prúdoch a ich pôvode

Aby sme pochopili viacvláknové vlákno, najskôr pochopme, čo je to proces. Proces je kus virtuálnej pamäte a zdrojov, ktoré OS alokuje na spustenie programu. Ak otvoríte niekoľko inštancií tej istej aplikácie, systém pre každú z nich priradí postup. V moderných prehliadačoch môže byť za každú kartu zodpovedný samostatný proces.

Pravdepodobne ste už narazili na „Správcu úloh“ systému Windows (v Linuxe je to „Monitor systému“) a viete, že nepotrebné spustené procesy zaťažia systém a tie „najťažšie“ z nich často zamrznú, takže ich musíte násilne ukončiť .

Používatelia však milujú multitasking: nekŕmte ich chlebom - nechajte ich otvoriť tucet okien a skákať tam a späť. Existuje dilema: musíte zabezpečiť súčasnú prevádzku aplikácií a súčasne znížiť zaťaženie systému, aby sa nespomalilo. Povedzme, že hardvér nemôže držať krok s potrebami vlastníkov - problém musíte vyriešiť na softvérovej úrovni.

Chceme, aby procesor vykonal viac pokynov a spracoval viac údajov za jednotku času. To znamená, že musíme vložiť viac spusteného kódu do každého časového segmentu. Myslite na jednotku spustenia kódu ako na objekt - to je vlákno.

K zložitému prípadu sa dostanete jednoduchšie, ak ho rozdelíte na niekoľko jednoduchých. Takže pri práci s pamäťou: „ťažký“ proces je rozdelený na vlákna, ktoré zaberajú menej zdrojov a je väčšia pravdepodobnosť, že doručia kód do kalkulačky (ako presne - pozri nižšie).

Každá aplikácia má najmenej jeden proces a každý proces má najmenej jedno vlákno, ktoré sa nazýva hlavné vlákno a z ktorého sa v prípade potreby spúšťajú nové.

Rozdiel medzi vláknami a procesmi

    Vlákna používajú pamäť pridelenú procesu a procesy vyžadujú svoj vlastný pamäťový priestor. Vlákna sa preto vytvárajú a dokončujú rýchlejšie: systém im nemusí zakaždým prideľovať nový adresný priestor a potom ho uvoľniť.

    Každý z procesov pracuje s vlastnými údajmi - môžu si niečo vymieňať iba prostredníctvom mechanizmu medziprocesovej komunikácie. Vlákna majú navzájom priamy prístup k údajom a zdrojom: to, čo sa jedno zmenilo, je okamžite k dispozícii každému. Vlákno môže v procese ovládať „kolegu“, zatiaľ čo proces riadi výlučne jeho „dcéry“. Prepínanie medzi streammi je preto rýchlejšie a komunikácia medzi nimi je jednoduchšia.

Aký je z toho záver? Ak potrebujete spracovať veľké množstvo údajov čo najrýchlejšie, rozdeľte ich na kúsky, ktoré je možné spracovať oddelenými vláknami, a potom spojte výsledok. Je to lepšie, ako vytvárať procesy náročné na zdroje.

Prečo sa však populárna aplikácia ako Firefox vydáva cestou vytvárania viacerých procesov? Pretože práve pre prehliadač funguje izolovaná karta spoľahlivo a flexibilne. Ak je s jedným procesom niečo v poriadku, nie je potrebné ukončovať celý program - je možné uložiť aspoň časť údajov.

Čo je viacvláknové

Dostávame sa teda k hlavnému bodu. Multithreading je, keď je proces aplikácie rozdelený na vlákna, ktoré sú spracované paralelne - v jednu jednotku času - procesorom.

Výpočtová záťaž je rozložená medzi dve alebo viac jadier, takže rozhranie a ďalšie súčasti programu navzájom nespomaľujú prácu.

Aplikácie s viacerými vláknami je možné spustiť na jednojadrových procesoroch, ale potom sa vlákna postupne vykonajú: prvý fungoval, jeho stav bol uložený - druhý mohol fungovať, uložený - vrátil sa k prvému alebo spustil tretí, atď.

Zaneprázdnení ľudia sa sťažujú, že majú iba dve ruky. Procesy a programy môžu mať toľko rúk, koľko je potrebné na čo najrýchlejšie splnenie úlohy.

Počkajte na signál: synchronizácia vo viacvláknových aplikáciách

Predstavte si, že niekoľko vlákien sa pokúša zmeniť rovnakú oblasť údajov súčasne. Koho zmeny budú nakoniec prijaté a koho zmeny budú zrušené? Aby sa zabránilo zmätku pri práci so zdieľanými zdrojmi, vlákna musia koordinovať svoje akcie. Za týmto účelom si vymieňajú informácie pomocou signálov. Každé vlákno hovorí ostatným, čo robí a aké zmeny môžu očakávať. Údaje všetkých vlákien o aktuálnom stave zdrojov sú teda synchronizované.

Základné synchronizačné nástroje

Vzájomné vylúčenie (vzájomné vylúčenie, skrátene - mutex) - „príznak“ smerujúci do vlákna, ktoré má v súčasnosti povolené pracovať so zdieľanými zdrojmi. Eliminuje prístup iných vlákien do obsadenej oblasti pamäte. V aplikácii môže byť niekoľko mutexov, ktoré je možné zdieľať medzi procesmi. Má to háčik: mutex núti aplikáciu pristupovať k jadru operačného systému zakaždým, čo je drahé.

Semafor - umožňuje obmedziť počet vlákien, ktoré majú v danom momente prístup k zdroju. Zníži sa tým zaťaženie procesora pri vykonávaní kódu, kde sú úzke miesta. Problém je v tom, že optimálny počet vlákien závisí od počítača používateľa.

Udalosť - definujete podmienku, pri ktorej výskyte sa kontrola prenesie do požadovaného vlákna. Toky si vymieňajú údaje o udalostiach, aby sa navzájom rozvíjali a logicky pokračovali vo svojich akciách. Jeden dostal údaje, druhý skontroloval ich správnosť, tretí ich uložil na pevný disk. Udalosti sa líšia v spôsobe ich zrušenia. Ak potrebujete na udalosť upozorniť niekoľko vlákien, budete musieť manuálne nastaviť funkciu zrušenia, aby sa signál zastavil. Ak existuje iba jedno cieľové vlákno, môžete vytvoriť udalosť automatického resetovania. Po dosiahnutí prúdu zastaví samotný signál. Udalosti je možné zaradiť do frontu na flexibilné riadenie toku.

Kritická časť - zložitejší mechanizmus, ktorý kombinuje počítadlo slučky a semafor. Počítadlo vám umožňuje odložiť začiatok semaforu na požadovaný čas. Výhodou je, že jadro sa aktivuje iba vtedy, ak je sekcia zaneprázdnená a je potrebné zapnúť semafor. Po zvyšok času vlákno beží v užívateľskom režime. Žiaľ, sekciu je možné použiť iba v rámci jedného procesu.

Ako implementovať multithreading v Jave

Trieda Thread je zodpovedná za prácu s vláknami v Jave. Vytvorenie nového vlákna na vykonanie úlohy znamená vytvorenie inštancie triedy Thread a priradenie k požadovanému kódu. To je možné vykonať dvoma spôsobmi:

    podtrieda Niť;

    implementujte vo svojej triede rozhranie Runnable a potom odovzdajte inštancie triedy konštruktérovi Thread.

Zatiaľ čo sa nebudeme dotýkať témy uviaznutia (uviaznutia), keď vlákna navzájom blokujú prácu a visia, necháme to na nasledujúci článok.

Príklad viacvláknového Java: ping-pong s mutexmi

Ak si myslíte, že sa stane niečo hrozné, vydýchnite si. Prácu so synchronizačnými objektmi zvážime takmer hravou formou: dve vlákna budú vrhané mutexom. V skutočnosti však uvidíte skutočnú aplikáciu, v ktorej môže naraz spracovávať verejne dostupné údaje iba jedno vlákno.

Najprv vytvoríme triedu, ktorá zdedí vlastnosti vlákna, ktoré už poznáme, a napíšeme metódu kickBall:

Verejná trieda PingPongThread rozširuje vlákno (PingPongThread (názov reťazca) (this.setName (názov); // prepíše názov vlákna) @Override public void run () (Ball ball = Ball.getBall (); while (ball.isInGame () ) (kickBall (lopta);)) súkromná prázdna kickBall (lopta) (ak (! ball.getSide (). rovná sa (getName ())) (ball.kick (getName ());))))

Teraz sa postarajme o loptu. Nebude s nami jednoduchý, ale nezabudnuteľný: aby vedel, kto ho udrel, z ktorej strany a koľkokrát. Na tento účel používame mutex: bude zhromažďovať informácie o práci každého z vlákien - to umožní izolovaným vláknam navzájom komunikovať. Po 15. údere vytiahneme loptu z hry, aby sme ju vážne nezranili.

Verejný ples (súkromné ​​kopy = 0; súkromná inštancia Ball Ball = new Ball (); private String side = ""; private Ball () () static Ball getBall () (inštancia návratu;) synchronizovaný neplatný kop (meno hráča s reťazcom) (kopy ++; strana = meno hráča; System.out.println (kopy + "" + strana);) Reťazec getSide () (návratová strana;) boolean isInGame () (návrat (kopy< 15); } }

A teraz vstupujú na scénu dve vlákna hráčov. Nazvime ich bez ďalších okolkov Ping a Pong:

Verejná trieda PingPongGame (PingPongThread player1 = nový PingPongThread ("Ping"); PingPongThread player2 = nový PingPongThread ("Pong"); Loptička; PingPongGame () (ball = Ball.getBall ();) neplatná štartGame () hodí InterruptedEx .start (); player2.start ();))

„Plný štadión ľudí - čas začať zápas.“ Oficiálne oznámime otvorenie stretnutia - v hlavnej triede aplikácie:

Verejná trieda PingPong (public static void main (String args) hodí InterruptedException (hra PingPongGame = new PingPongGame (); game.startGame ();))

Ako vidíte, nie je tu nič zúrivé. Toto je zatiaľ len úvod do multithreadingu, ale vy už viete, ako to funguje, a môžete experimentovať - ​​obmedzte trvanie hry nie počtom úderov, ale napríklad časom. K téme multithreading sa vrátime neskôr - pozrieme sa na balík java.util.concurrent, knižnicu Akka a mechanizmus volatile. Porozprávajme sa tiež o implementácii viacvláknového spracovania v Pythone.

Viacvláknové programovanie sa zásadne nelíši od písania grafických používateľských rozhraní riadených udalosťami alebo dokonca od písania jednoduchých sekvenčných aplikácií. Platia tu všetky dôležité pravidlá upravujúce zapuzdrenie, oddelenie obáv, voľné spojenie atď. Mnoho vývojárov však považuje za ťažké písať viacvláknové programy práve preto, že tieto pravidlá zanedbávajú. Namiesto toho sa pokúšajú uplatniť v praxi oveľa menej dôležité znalosti o vláknach a synchronizačných primitívach, zozbierané z textov o viacvláknovom programovaní pre začiatočníkov.

Aké sú teda tieto pravidlá

Ďalší programátor, ktorý čelí problému, si myslí: „Ach, presne, musíme použiť regulárne výrazy.“ A teraz má už dva problémy - Jamie Zawinski.

Ďalší programátor, konfrontovaný s problémom, si myslí: „Ach, dobre, tu použijem streamy.“ A teraz má desať problémov - Bill Schindler.

Príliš veľa programátorov, ktorí sa zaviažu písať viacvláknový kód, sa dostane do pasce, ako hrdina Goetheho balady “ Čarodejníkov učeň“. Programátor sa naučí vytvárať zväzok vlákien, ktoré v zásade fungujú, ale skôr alebo neskôr sa im vymknú spod kontroly a programátor nevie, čo má robiť.

Ale na rozdiel od čarodejníka, ktorý prestal pracovať, nešťastný programátor nemôže dúfať v príchod mocného čarodejníka, ktorý mávne prútikom a obnoví poriadok. Namiesto toho programátor hľadá tie najnepeknejšie triky a snaží sa vyrovnať s neustále sa objavujúcimi problémami. Výsledok je vždy rovnaký: získa sa príliš komplikovaná, obmedzená, krehká a nespoľahlivá aplikácia. Existuje večná hrozba zablokovania a ďalšie nebezpečenstvá súvisiace so zlým viacvláknovým kódom. Nehovorím ani o nevysvetlených pádoch, slabom výkone, neúplných alebo nesprávnych pracovných výsledkoch.

Možno vás zaujíma: prečo sa to deje? Bežná mylná predstava je: „Viacvláknové programovanie je veľmi ťažké.“ Ale nie je tomu tak. Ak je viacvláknový program nespoľahlivý, potom spravidla zlyhá z rovnakých dôvodov ako nekvalitný jednovláknový program. Programátor jednoducho nedodržiava základné, známe a osvedčené metódy vývoja. Viacvláknové programy sa zdajú byť iba komplexnejšie, pretože čím viac súbežných vlákien sa pokazí, tým viac neporiadku narobia - a oveľa rýchlejšie, ako by to urobilo jedno vlákno.

Mylná predstava o „zložitosti viacvláknového programovania“ sa rozšírila kvôli tým vývojárom, ktorí sa profesionálne vyvinuli v písaní jednovláknového kódu, ktorí sa prvýkrát stretli s viacvláknovým procesom a nevyrovnali sa s ním. Ale namiesto toho, aby prehodnotili svoje zaujatosti a pracovné návyky, tvrdohlavo opravujú skutočnosť, že nechcú nijako pracovať. Títo ľudia, ktorí sa ospravedlňujú nespoľahlivým softvérom a zmeškanými termínmi, opakujú to isté: „viacvláknové programovanie je veľmi ťažké“.

Upozorňujeme, že vyššie hovorím o typických programoch, ktoré používajú viacvláknové vlákno. Skutočne existujú komplexné viacvláknové scenáre-ako aj zložité jednovláknové scenáre. Ale nie sú bežné. V praxi sa od programátora spravidla nevyžaduje nič nadprirodzené. Presúvame údaje, transformujeme ich, z času na čas vykonáme nejaké výpočty a nakoniec uložíme informácie do databázy alebo ich zobrazíme na obrazovke.

Nie je nič ťažké zlepšiť priemerný jednovláknový program a urobiť z neho viacvláknový. Aspoň by nemalo byť. Ťažkosti majú dva dôvody:

  • programátori nevedia aplikovať jednoduché, dobre osvedčené metódy vývoja;
  • väčšina informácií uvedených v knihách o viacvláknovom programovaní je technicky správna, ale na riešenie aplikovaných problémov úplne nepoužiteľná.

Najdôležitejšie koncepty programovania sú univerzálne. Sú rovnako použiteľné pre jednovláknové aj viacvláknové programy. Programátori topiaci sa vo víre prúdov sa jednoducho nenaučili dôležité lekcie, keď ovládali jednovláknový kód. Môžem to povedať, pretože takíto vývojári robia rovnaké zásadné chyby vo viacvláknových a jednovláknových programoch.

Asi najdôležitejšia lekcia, ktorú si musíte vziať za šesťdesiat rokov histórie programovania, je: globálny premenlivý stav- zlo... Skutočné zlo. Programy, ktoré sa spoliehajú na globálne mutovateľný stav, je relatívne ťažké odôvodniť a vo všeobecnosti sú nespoľahlivé, pretože existuje príliš veľa spôsobov, ako zmeniť stav. Existuje mnoho štúdií, ktoré potvrdzujú tento všeobecný princíp, existuje nespočetné množstvo návrhových vzorov, ktorých hlavným cieľom je implementovať jeden alebo iný spôsob skrývania údajov. Aby boli vaše programy predvídateľnejšie, pokúste sa čo najviac eliminovať premenlivé stavy.

V sekvenčnom programe s jedným vláknom je pravdepodobnosť poškodenia údajov priamo úmerná počtu komponentov, ktoré môžu údaje upravovať.

Globálneho stavu nie je spravidla možné úplne zbaviť, ale vývojár má vo svojom arzenáli veľmi účinné nástroje, ktoré vám umožňujú prísne kontrolovať, ktoré súčasti programu môžu stav zmeniť. Okrem toho sme sa naučili vytvárať reštriktívne vrstvy API okolo primitívnych dátových štruktúr. Preto máme dobrú kontrolu nad tým, ako sa tieto dátové štruktúry menia.

Problémy globálne premenlivého stavu sa postupne začali prejavovať koncom 80. a začiatkom 90. rokov s nárastom programovania riadeného udalosťami. Programy sa už nespustili „od začiatku“ alebo nesledovali jednu predvídateľnú cestu vykonávania „až do konca“. Moderné programy majú počiatočný stav, po ukončení ktorého sa v nich dejú udalosti - v nepredvídateľnom poradí, s premenlivými časovými intervalmi. Kód zostáva s jedným vláknom, ale už sa stáva asynchrónnym. Pravdepodobnosť poškodenia údajov sa zvyšuje práve preto, že poradie výskytu udalostí je veľmi dôležité. Situácie tohto druhu sú celkom bežné: ak nastane udalosť B po udalosti A, potom všetko funguje dobre. Ak však k udalosti A dôjde po udalosti B a udalosť C má čas zasiahnuť medzi nimi, údaje môžu byť skreslené na nepoznanie.

Ak ide o paralelné toky, problém sa ešte zhoršuje, pretože v globálnom stave môže súčasne fungovať niekoľko metód. Je nemožné presne posúdiť, ako sa globálny stav mení. Hovoríme už nielen o skutočnosti, že udalosti môžu nastať v nepredvídateľnom poradí, ale aj o tom, že je možné aktualizovať stav niekoľkých vlákien vykonávania. súčasne... Pri asynchrónnom programovaní môžete prinajmenšom zaistiť, aby sa určitá udalosť nestala skôr, ako iná udalosť dokončí spracovanie. To znamená, že je možné s istotou povedať, aký bude globálny stav na konci spracovania konkrétnej udalosti. Vo viacvláknovom kóde je spravidla nemožné určiť, ktoré udalosti sa vyskytnú súbežne, takže nie je možné s istotou popísať globálny stav v akomkoľvek danom čase.

Viacvláknový program s rozsiahlym globálne premenlivým stavom je jedným z najvýstižnejších príkladov Heisenbergovho princípu neurčitosti, o ktorom viem. Nie je možné skontrolovať stav programu bez zmeny jeho správania.

Keď začnem ďalšiu filipiku o globálnom premenlivom stave (podstata je načrtnutá v niekoľkých predchádzajúcich odsekoch), programátori prevrátia očami a uistia ma, že to všetko už dávno vedia. Ale ak to viete, prečo to nemôžete zistiť z kódu? Programy sú preplnené globálnym premenlivým stavom a programátori sa čudujú, prečo kód nefunguje.

Nie je prekvapením, že najdôležitejšia práca vo viacvláknovom programovaní prebieha vo fáze návrhu. Je potrebné jasne definovať, čo by program mal robiť, vyvinúť nezávislé moduly na vykonávanie všetkých funkcií, podrobne popísať, aké údaje sú pre ktorý modul potrebné, a určiť spôsoby výmeny informácií medzi modulmi ( Áno, nezabudnite pripraviť pekné tričká pre všetkých, ktorí sa na projekte podieľajú. Prvá vec.- približne. vyd. v origináli). Tento proces sa zásadne nelíši od navrhovania programu s jedným vláknom. Kľúčom k úspechu, rovnako ako u jednovláknového kódu, je obmedziť interakcie medzi modulmi. Ak sa môžete zbaviť zdieľaného premenlivého stavu, problémy so zdieľaním údajov jednoducho nevzniknú.

Niekto by mohol namietať, že niekedy nie je čas na taký delikátny dizajn programu, ktorý umožní zaobísť sa bez globálneho štátu. Verím, že je možné a nevyhnutné venovať tomu čas. Nič neovplyvňuje viacvláknové programy tak deštruktívne ako snaha vyrovnať sa s globálnym premenlivým stavom. Čím viac podrobností musíte zvládnuť, tým je väčšia pravdepodobnosť, že váš program dosiahne vrchol a zlyhá.

V realistických aplikáciách musí existovať určitý zdieľaný stav, ktorý sa môže zmeniť. A tu začína väčšina programátorov mať problémy. Programátor vidí, že je tu potrebný zdieľaný stav, obráti sa na viacvláknový arzenál a vezme si odtiaľ najjednoduchší nástroj: univerzálny zámok (kritická sekcia, mutex alebo ako to nazývajú). Zdá sa, že veria, že vzájomné vylúčenie vyrieši všetky problémy so zdieľaním údajov.

Počet problémov, ktoré môžu pri takom jedinom zámku nastať, je ohromujúci. Je potrebné vziať do úvahy závodné podmienky, problémy s bránením s príliš rozsiahlym blokovaním a otázky spravodlivosti alokácie sú len niekoľkými príkladmi. Ak máte viacero zámkov, najmä ak sú vnorené, budete musieť tiež prijať opatrenia proti zablokovaniu, dynamickému zablokovaniu, blokovaniu frontov a iným hrozbám spojeným so súbežnosťou. Okrem toho existujú aj inherentné problémy jednoduchého blokovania.
Keď píšem alebo kontrolujem kód, mám takmer neomylné železné pravidlo: ak ste urobili zámok, zdá sa, že ste niekde urobili chybu.

Toto tvrdenie je možné komentovať dvoma spôsobmi:

  1. Ak potrebujete uzamknutie, pravdepodobne máte globálny zmeniteľný stav, ktorý chcete chrániť pred súbežnými aktualizáciami. Prítomnosť globálneho mutovateľného stavu je chybou vo fáze návrhu aplikácie. Kontrola a redizajn.
  2. Správne používanie zámkov nie je jednoduché a lokalizovať chyby súvisiace s uzamykaním môže byť neuveriteľne ťažké. Je veľmi pravdepodobné, že zámok budete používať nesprávne. Ak vidím zámok a program sa správa neobvyklým spôsobom, prvá vec, ktorú urobím, je skontrolovať kód, ktorý závisí od zámku. A väčšinou v tom nachádzam problémy.

Oba tieto výklady sú správne.

Písanie viacvláknového kódu je jednoduché. Je však veľmi, veľmi ťažké správne používať synchronizačné primitívy. Pravdepodobne nie ste kvalifikovaní používať ani jeden zámok správne. Koniec koncov, zámky a ďalšie synchronizačné primitívy sú konštrukcie postavené na úrovni celého systému. Ľudia, ktorí chápu paralelné programovanie oveľa lepšie ako vy, používajú tieto primitíva na vytváranie súbežných dátových štruktúr a synchronizačných štruktúr na vysokej úrovni. A ty a ja, obyčajní programátori, berieme takéto konštrukcie a používame ich v našom kóde. Programátor aplikácií by nemal používať primitíva synchronizácie na nízkej úrovni častejšie, ako priamo volá na ovládače zariadení. Teda takmer nikdy.

Pokúšať sa používať zámky na riešenie problémov so zdieľaním údajov je ako hasiť oheň tekutým kyslíkom. Rovnako ako požiaru, takýmto problémom je jednoduchšie predchádzať než ich opravovať. Ak sa zbavíte zdieľaného stavu, nemusíte ani zneužívať synchronizačné primitívy.

Väčšina toho, čo viete o multithreadingu, je irelevantná

Vo viacvláknových návodoch pre začiatočníkov sa dozviete, čo sú to vlákna. Potom autor začne zvažovať rôzne spôsoby, ktorými môžu tieto vlákna fungovať paralelne - napríklad hovoriť o riadení prístupu k zdieľaným údajom pomocou zámkov a semaforov a zaoberať sa tým, čo sa môže stať pri práci s udalosťami. Podrobne sa pozrie na premenné stavu, bariéry pamäte, kritické sekcie, mutexy, prchavé polia a atómové operácie. Budú prediskutované príklady toho, ako používať tieto nízkoúrovňové konštrukcie na vykonávanie všetkých druhov systémových operácií. Po prečítaní tohto materiálu na polovicu sa programátor rozhodne, že už vie dosť o všetkých týchto primitívach a ich použití. Napokon, ak viem, ako táto vec funguje na systémovej úrovni, môžem ju rovnako uplatniť aj na aplikačnej úrovni. Áno?

Predstavte si, že by ste teenagerovi povedali, ako si sám zostaví spaľovací motor. Potom ho bez akéhokoľvek vodičského výcviku posadíte za volant auta a poviete: „Choďte!“ Tínedžer chápe, ako auto funguje, ale netuší, ako sa na ňom dostať z bodu A do bodu B.

Pochopenie toho, ako vlákna fungujú na systémovej úrovni, zvyčajne na úrovni aplikácie nijako nepomáha. Netvrdím, že programátori sa nemusia učiť všetky tieto detaily na nízkej úrovni. Len nečakajte, že tieto znalosti budete môcť ihneď použiť pri navrhovaní alebo vývoji obchodnej aplikácie.

Úvodná vláknová literatúra (a súvisiace akademické kurzy) by nemala skúmať takéto nízkoúrovňové konštrukcie. Musíte sa zamerať na riešenie najbežnejších tried problémov a ukázať vývojárom, ako sú tieto problémy riešené pomocou schopností na vysokej úrovni. V zásade je väčšina podnikových aplikácií extrémne jednoduchými programami. Čítajú údaje z jedného alebo viacerých vstupných zariadení, vykonávajú s nimi komplexné spracovanie (napríklad v procese požadujú ďalšie údaje) a potom odosielajú výsledky.

Tieto programy často perfektne zapadajú do modelu poskytovateľa a spotrebiteľa, ktorý vyžaduje iba tri vlákna:

  • vstupný tok načíta údaje a zaradí ich do vstupného frontu;
  • pracovné vlákno číta záznamy zo vstupného frontu, spracúva ich a výsledky vkladá do výstupného frontu;
  • výstupný prúd číta položky z výstupného frontu a ukladá ich.

Tieto tri vlákna fungujú nezávisle, komunikácia medzi nimi prebieha na úrovni frontu.

Aj keď technicky možno tieto rady považovať za zóny zdieľaného stavu, v praxi sú to len komunikačné kanály, v ktorých funguje ich vlastná vnútorná synchronizácia. Fronty podporujú spoluprácu s mnohými výrobcami a spotrebiteľmi naraz, položky v nich môžete pridávať a odstraňovať súbežne.

Pretože sú vstupné, spracovateľské a výstupné fázy navzájom izolované, ich implementáciu je možné ľahko zmeniť bez ovplyvnenia zvyšku programu. Pokiaľ sa typ údajov vo fronte nezmení, môžete jednotlivé komponenty programu refaktorovať podľa vlastného uváženia. Navyše, keďže sa na rade zúčastňuje ľubovoľný počet dodávateľov a spotrebiteľov, nie je ťažké pridať ďalších výrobcov / spotrebiteľov. Môžeme mať desiatky vstupných tokov, ktoré zapisujú informácie do rovnakého frontu, alebo desiatky pracovných vlákien, ktoré preberajú informácie zo vstupného frontu a trávia údaje. V rámci jedného počítača sa takýto model dobre škáluje.

A čo je najdôležitejšie, moderné programovacie jazyky a knižnice veľmi uľahčujú vytváranie aplikácií pre výrobcov a spotrebiteľov. V .NET nájdete paralelné zbierky a knižnicu toku údajov TPL. Java má službu Executor, ako aj BlockingQueue a ďalšie triedy z priestoru názvov java.util.concurrent. C ++ má knižnicu vlákien Boost a knižnicu Intel Thread Building Blocks. Microsoft Visual Studio 2013 predstavuje asynchrónne agenty. Podobné knižnice sú k dispozícii aj v jazykoch Python, JavaScript, Ruby, PHP a pokiaľ viem, v mnohých ďalších jazykoch. Aplikáciu výrobca-spotrebiteľ môžete vytvoriť pomocou ktoréhokoľvek z týchto balíkov bez toho, aby ste sa museli uchýliť k zámkom, semaforom, podmienkovým premenným alebo iným synchronizačným primitívom.

V týchto knižniciach sa voľne používa široká škála synchronizačných primitívov. Toto je fajn. Všetky tieto knižnice sú napísané ľuďmi, ktorí rozumejú multithreadingu neporovnateľne lepšie ako priemerný programátor. Práca s takouto knižnicou je prakticky rovnaká ako používanie knižnice runtime jazykov. To sa dá porovnať s programovaním v jazyku na vysokej úrovni, a nie v montážnom jazyku.

Dodávateľsko-spotrebiteľský model je len jedným z mnohých príkladov. Vyššie uvedené knižnice obsahujú triedy, ktoré je možné použiť na implementáciu mnohých bežných vzorov navrhovania závitov bez toho, aby sa zachádzalo do podrobností nízkej úrovne. Je možné vytvárať rozsiahle viacvláknové aplikácie bez obáv z toho, ako sú vlákna koordinované a synchronizované.

Práca s knižnicami

Vytváranie viacvláknových programov sa teda zásadne nelíši od písania synchrónnych programov s jedným vláknom. Dôležité zásady zapuzdrenia a skrývania údajov sú univerzálne a ich dôležitosť narastá, iba ak sú zapojené viaceré súbežné vlákna. Ak tieto dôležité aspekty zanedbáte, potom vás ani najkomplexnejšia znalosť závitovania na nízkej úrovni nezachráni.

Moderní vývojári musia vyriešiť veľa problémov na úrovni programovania aplikácií, stáva sa, že jednoducho nie je čas premýšľať o tom, čo sa deje na úrovni systému. Čím sú aplikácie zložitejšie, tým zložitejšie detaily je potrebné skrývať medzi úrovňami API. Robíme to viac ako tucet rokov. Možno tvrdiť, že kvalitatívne utajenie zložitosti systému pred programátorom je hlavným dôvodom, prečo je programátor schopný písať moderné aplikácie. Čo sa týka toho, neskrývame zložitosť systému implementáciou slučky správ UI, vytváraním komunikačných protokolov nízkej úrovne atď.?

Podobná situácia je aj pri multithreadingu. Väčšina viacvláknových scenárov, s ktorými sa môže priemerný programátor podnikových aplikácií stretnúť, je už dobre známych a dobre implementovaných v knižniciach. Knižničné funkcie robia skvelú prácu pri skrývaní drvivej komplexnosti rovnobežnosti. Musíte sa naučiť používať tieto knižnice rovnakým spôsobom, akým používate knižnice prvkov používateľského rozhrania, komunikačné protokoly a množstvo ďalších nástrojov, ktoré fungujú. Nechajte viacvláknové spracovanie na nízkej úrovni na odborníkoch - autoroch knižníc používaných pri tvorbe aplikácií.

NS Tento článok nie je pre vytrvalých krotiteľov Pythonu, pre ktorých je rozpletanie tejto gule hadov detskou hrou, ale skôr povrchný prehľad viacvláknových schopností pre novo závislého pythona.

V ruštine bohužiaľ nie je toľko materiálu na tému multithreading v Pythone a pythoni, ktorí nič nepočuli napríklad o GIL, mi začali závidieť závideniahodnou pravidelnosťou. V tomto článku sa pokúsim popísať najzákladnejšie vlastnosti viacvláknového pythonu, povedať vám, čo je GIL a ako s ním žiť (alebo bez neho) a mnoho ďalších.


Python je očarujúci programovací jazyk. Dokonale kombinuje mnoho paradigiem programovania. Väčšina úloh, s ktorými sa môže programátor stretnúť, je tu vyriešená jednoducho, elegantne a stručne. Ale pre všetky tieto problémy často postačuje riešenie s jedným vláknom a programy s jedným vláknom sú zvyčajne predvídateľné a ľahko sa ladia. To isté sa nedá povedať o viacvláknových a viacprocesových programoch.

Viacvláknové aplikácie


Python má modul navliekanie , a má všetko, čo potrebujete na viacvláknové programovanie: existujú rôzne typy zámkov, semafor a mechanizmus udalostí. Jedným slovom - všetko, čo je potrebné pre drvivú väčšinu viacvláknových programov. Používanie všetkých týchto nástrojov je navyše veľmi jednoduché. Uvažujme o príklade programu, ktorý spustí 2 vlákna. Jedno vlákno napíše desať „0“, druhé - desať „1“ a striktne zasa.

import vlákna

def spisovateľ

pre i v xrange (10):

vytlačiť x

Event_for_set.set ()

# inicializačné udalosti

e1 = navliekanie. Udalosť ()

e2 = navliekanie. Udalosť ()

# inicializačné vlákna

0, e1, e2))

1, e2, e1))

# začnite vlákna

t1.start ()

t2.start ()

t1.join ()

t2.join ()


Žiadna mágia ani kód voodoo. Kód je jasný a konzistentný. Navyše, ako vidíte, vytvorili sme stream z funkcie. To je veľmi výhodné pre malé úlohy. Tento kód je tiež dosť flexibilný. Predpokladajme, že máme tretí proces, ktorý píše „2“, potom bude kód vyzerať takto:

import vlákna

def spisovateľ (x, event_for_wait, event_for_set):

pre i v xrange (10):

Event_for_wait.wait () # čakať na udalosť

Event_for_wait.clear () # čistá udalosť pre budúcnosť

vytlačiť x

Event_for_set.set () # set udalosť pre vlákno suseda

# inicializačné udalosti

e1 = navliekanie. Udalosť ()

e2 = navliekanie. Udalosť ()

e3 = navliekanie. Udalosť ()

# inicializačné vlákna

t1 = vlákno. Vlákno (cieľ = zapisovateľ, args = ( 0, e1, e2))

t2 = vlákno. Vlákno (cieľ = zapisovateľ, args = ( 1, e2, e3))

t3 = vlákno. Vlákno (cieľ = zapisovateľ, args = ( 2, e3, e1))

# začnite vlákna

t1.start ()

t2.start ()

t3.start ()

e1.set () # spustí prvú udalosť

# pripojiť vlákna k hlavnému vláknu

t1.join ()

t2.join ()

t3.join ()


Pridali sme novú udalosť, nové vlákno a mierne pozmenili parametre, s ktorými
prúdy sa začínajú (samozrejme, môžete napísať všeobecnejšie riešenie napríklad pomocou MapReduce, ale to je nad rámec tohto článku).
Ako vidíte, stále neexistuje žiadna mágia. Všetko je jednoduché a priamočiare. Poďme ďalej.

Globálny zámok tlmočníka


Existujú dva najbežnejšie dôvody, prečo používať vlákna: po prvé, zvýšiť efektivitu používania viacjadrovej architektúry moderných procesorov, a tým aj výkon programu;
za druhé, ak potrebujeme rozdeliť logiku programu na paralelné, úplne alebo čiastočne asynchrónne sekcie (napríklad aby sme mohli pingovať súčasne na niekoľko serverov).

V prvom prípade stojíme pred takýmto obmedzením Pythonu (alebo skôr jeho hlavnou implementáciou CPython), ako je napríklad Global Interpreter Lock (alebo skrátene GIL). Koncepcia GIL je taká, že procesor môže naraz vykonávať iba jedno vlákno. To sa deje tak, že neexistuje žiadny boj medzi vláknami o oddelené premenné. Spustiteľné vlákno získa prístup do celého prostredia. Táto funkcia implementácie vlákna v Pythone výrazne zjednodušuje prácu s vláknami a zaisťuje určitú bezpečnosť vlákna.

Existuje však jemný bod: Mohlo by sa zdať, že viacvláknová aplikácia pobeží úplne rovnako dlho ako jednovláknová aplikácia, ktorá robí to isté, alebo súčet času vykonávania každého vlákna v CPU. Tu nás však čaká jeden nepríjemný efekt. Zvážte program:

s otvoreným ("test1.txt", "w") ako nasledujúcim:

pre i v xrange (10 000 000):

tlač >> fout, 1


Tento program zapíše do súboru milión riadkov „1“ a v počítači to urobí ~ 0,35 sekundy.

Zvážte iný program:

z vlákna importovať vlákno

def spisovateľ (názov súboru, n):

s otvoreným (názov súboru, "w") ako nasledujúcim:

pre i v xrange (n):

tlač >> fout, 1

t1 = vlákno (cieľ = zapisovateľ, args = ("test2.txt", 500 000,))

t2 = Vlákno (cieľ = zapisovateľ, args = ("test3.txt", 500 000,))

t1.start ()

t2.start ()

t1.join ()

t2.join ()


Tento program vytvorí 2 vlákna. V každom vlákne zapíše do samostatného súboru pol milióna riadkov „1“. V skutočnosti je množstvo práce rovnaké ako v predchádzajúcom programe. Časom sa tu však dosiahne zaujímavý efekt. Program môže trvať od 0,7 sekundy do 7 sekúnd. Prečo sa to deje?

Dôvodom je skutočnosť, že keď vlákno nepotrebuje zdroj CPU, uvoľní GIL a v tomto okamihu sa môže pokúsiť získať ho a ďalšie vlákno a tiež hlavné vlákno. Operačný systém, ktorý vie, že jadier je veľa, môže všetko zhoršiť tým, že sa pokúsi distribuovať vlákna medzi jadrami.

UPD: v súčasnosti je v Pythone 3.2 vylepšená implementácia GIL, v ktorej je tento problém čiastočne vyriešený, najmä kvôli skutočnosti, že každé vlákno po strate kontroly čaká na krátke obdobie pred ním. môže opäť zachytiť GIL (existuje dobrá prezentácia v angličtine)

"Takže nemôžete písať efektívne viacvláknové programy v Pythone?" Pýtate sa. Nie, samozrejme, existuje východisko, a dokonca aj niekoľko.

Viacprocesové aplikácie


Aby sa v určitom zmysle vyriešil problém popísaný v predchádzajúcom odseku, Python má modul podproces ... Môžeme napísať program, ktorý chceme spustiť v paralelnom vlákne (v skutočnosti je to už proces). A spustite ho v jednom alebo viacerých vláknach v inom programe. To by náš program skutočne urýchlilo, pretože vlákna vytvorené v spúšťači GIL sa nezdvíhajú, ale iba čakajú na ukončenie prebiehajúceho procesu. Táto metóda má však veľa problémov. Hlavným problémom je, že je ťažké prenášať údaje medzi procesmi. Museli by ste nejako serializovať objekty, nadviazať komunikáciu prostredníctvom PIPE alebo iných nástrojov, ale to všetko nevyhnutne nesie réžiu a kód sa stáva ťažko zrozumiteľným.

Tu nám môže pomôcť iný prístup. Python má viacprocesový modul ... Pokiaľ ide o funkčnosť, tento modul sa podobá navliekanie ... Napríklad procesy je možné vytvárať rovnakým spôsobom z bežných funkcií. Metódy pre prácu s procesmi sú takmer rovnaké ako pre vlákna z vláknového modulu. Je však zvykom používať na synchronizáciu procesov a výmenu údajov iné nástroje. Hovoríme o frontoch (Fronta) a potrubiach (Pipe). Sú tu však aj analógy zámkov, udalostí a semaforov, ktoré boli v navliekaní.

Modul multiprocesingu má navyše mechanizmus na prácu so zdieľanou pamäťou. Na tento účel má modul triedy premennej (hodnota) a poľa (pole), ktoré je možné „zdieľať“ medzi procesmi. Na uľahčenie práce so zdieľanými premennými môžete použiť triedy manažéra. Sú flexibilnejšie a jednoduchšie na používanie, ale pomalšie. Je potrebné poznamenať, že existuje pekná príležitosť na výrobu bežných typov z modulu ctypes pomocou modulu multiprocessing.sharedctypes.

Aj v multiprocesovom module je mechanizmus na vytváranie oblastí procesov. Tento mechanizmus je veľmi vhodné použiť na implementáciu vzoru Master-Worker alebo na implementáciu paralelnej mapy (čo je v istom zmysle špeciálny prípad Master-Worker).

Z hlavných problémov pri práci s viacprocesovým modulom stojí za zmienku relatívna závislosť tohto modulu na platforme. Pretože práca s procesmi je v rôznych operačných systémoch organizovaná odlišne, sú na kód kladené určité obmedzenia. Napríklad systém Windows nemá vidlicový mechanizmus, takže bod oddelenia procesu musí byť zabalený do:

if __name__ == "__main__":


Tento dizajn je však už dobrou formou.

Čo ešte...


V Pythone existujú aj ďalšie knižnice a prístupy na písanie paralelných aplikácií. Môžete napríklad použiť Hadoop + Python alebo rôzne implementácie MPI Pythonu (pyMPI, mpi4py). Môžete dokonca použiť obaly existujúcich knižníc C ++ alebo Fortran. Tu by sme mohli spomenúť také rámce / knižnice ako Pyro, Twisted, Tornado a mnoho ďalších. Ale to všetko už presahuje rámec tohto článku.

Ak sa vám páčil môj štýl, v nasledujúcom článku sa vám pokúsim povedať, ako písať jednoduchých tlmočníkov v PLY a na čo sa dajú použiť.

Kapitola 10.

Viacvláknové aplikácie

Multitasking v moderných operačných systémoch je samozrejmosťou [ Pred príchodom systému Apple OS X neexistovali na počítačoch Macintosh žiadne moderné viacúlohové operačné systémy. Je veľmi ťažké správne navrhnúť operačný systém s plnohodnotným multitaskingom, preto OS X musel vychádzať zo systému Unix.]. Užívateľ očakáva, že pri súčasnom spustení textového editora a poštového klienta nebudú tieto programy v konflikte a pri príjme e-mailu editor neprestane fungovať. Keď je spustených niekoľko programov súčasne, operačný systém rýchlo prepína medzi programami a poskytuje im postupne procesor (pokiaľ v počítači samozrejme nie je nainštalovaných viac procesorov). Ako výsledok, ilúzia spustenie viacerých programov súčasne, pretože ani ten najlepší pisár (a najrýchlejšie internetové pripojenie) nemôže držať krok s moderným procesorom.

Multithreading, v istom zmysle, môže byť považovaný za ďalšiu úroveň viacúlohového spracovania: namiesto prepínania medzi rôznymi programy, operačný systém prepína medzi rôznymi časťami toho istého programu. E-mailový klient s viacerými vláknami vám napríklad umožňuje prijímať nové e-mailové správy počas čítania alebo vytvárania nových správ. V dnešnej dobe už mnoho používateľov považuje za samozrejmé aj viacvláknové spracovanie.

VB nikdy nemalo normálnu podporu viacerých vlákien. Je pravda, že jedna z jeho odrôd sa objavila vo VB5 - kolaboratívny streamovací model(navliekanie bytu). Ako čoskoro uvidíte, kolaboratívny model poskytuje programátorovi niektoré výhody multithreadingu, ale nevyužíva všetky výhody všetkých funkcií. Skôr alebo neskôr sa musíte zmeniť z cvičného stroja na skutočný a VB .NET sa stala prvou verziou VB s podporou bezplatného viacvláknového modelu.

Viacvláknové vlákno však nepatrí medzi funkcie, ktoré sú v programovacích jazykoch ľahko implementované a programátormi ich ľahko zvládajú. Prečo?

Pretože vo viacvláknových aplikáciách môžu nastať veľmi záludné chyby, ktoré sa objavujú a miznú nepredvídateľne (a také chyby sa najľahšie ladia).

Úprimne varujúce: multithreading je jednou z najťažších oblastí programovania. Najmenšia nepozornosť vedie k vzniku nepolapiteľných chýb, ktorých oprava vyžaduje astronomické sumy. Z tohto dôvodu táto kapitola obsahuje mnoho zle príklady - zámerne sme ich napísali tak, aby demonštrovali bežné chyby. Toto je najbezpečnejší prístup k učeniu sa viacvláknového programovania: musíte byť schopní rozpoznať potenciálne problémy, keď sa na prvý pohľad zdá, že všetko funguje dobre, a vedieť ich vyriešiť. Ak chcete použiť viacvláknové programovacie techniky, bez toho sa nezaobídete.

Táto kapitola položí solídny základ pre ďalšiu nezávislú prácu, ale nebudeme schopní popísať viacvláknové programovanie vo všetkých zložitostiach - iba tlačená dokumentácia k triedam priestoru názvov Threading trvá viac ako 100 strán. Ak chcete zvládnuť viacvláknové programovanie na vyššej úrovni, pozrite sa do špecializovaných kníh.

Ale bez ohľadu na to, aké nebezpečné je viacvláknové programovanie, je nevyhnutné pre profesionálne riešenie niektorých problémov. Ak vaše programy nepoužívajú viacvláknové vlákno tam, kde je to vhodné, používatelia budú veľmi frustrovaní a uprednostnia iný produkt. Napríklad iba vo štvrtej verzii populárneho e-mailového programu Eudora sa objavili viacvláknové možnosti, bez ktorých si nemožno predstaviť žiadny moderný e-mailový program. V čase, keď Eudora predstavila podporu pre vytváranie viacerých vlákien, mnoho používateľov (vrátane jedného z autorov tejto knihy) prešlo na iné produkty.

Nakoniec v .NET programy s jedným vláknom jednoducho neexistujú. Všetko Programy .NET sú viacvláknové, pretože zberač odpadkov beží ako proces na pozadí s nízkou prioritou. Ako je uvedené nižšie, v prípade seriózneho grafického programovania v .NET môže správne vytváranie vlákien pomôcť zabrániť blokovaniu grafického rozhrania, keď program vykonáva zdĺhavé operácie.

Predstavujeme viacvláknové spracovanie

Každý program funguje v určitom kontext, popis distribúcie kódu a údajov v pamäti. Uložením kontextu sa skutočne uloží stav toku programu, čo vám umožní v budúcnosti ho obnoviť a pokračovať vo vykonávaní programu.

Ukladanie kontextu prináša náklady na čas a pamäť. Operačný systém si pamätá stav vlákna programu a prenáša riadenie do iného vlákna. Keď chce program pokračovať vo vykonávaní pozastaveného vlákna, musí byť uložený kontext obnovený, čo trvá ešte dlhšie. Viacvláknové vlákno by sa preto malo používať iba vtedy, ak prínosy kompenzujú všetky náklady. Niektoré typické príklady sú uvedené nižšie.

  • Funkčnosť programu je prehľadne a prirodzene rozdelená do niekoľkých heterogénnych operácií, ako v príklade s prijímaním e-mailov a prípravou nových správ.
  • Program vykonáva dlhé a zložité výpočty a nechcete, aby bolo grafické rozhranie počas výpočtov blokované.
  • Program beží na viacprocesorovom počítači s operačným systémom, ktorý podporuje používanie viacerých procesorov (pokiaľ počet aktívnych vlákien nepresiahne počet procesorov, paralelné vykonávanie je prakticky bez nákladov spojených s prepínaním vlákien).

Predtým, ako prejdeme k mechanike viacvláknových programov, je potrebné poukázať na jednu okolnosť, ktorá často spôsobuje zmätok medzi začiatočníkmi v oblasti viacvláknového programovania.

V toku programu sa vykoná procedúra, nie objekt.

Je ťažké povedať, čo sa myslí výrazom „objekt beží“, ale jeden z autorov často vedie semináre o viacvláknovom programovaní a táto otázka sa kladie častejšie ako ostatné. Možno si niekto myslí, že práca vlákna programu začína volaním novej metódy triedy, po ktorej vlákno spracováva všetky správy odovzdané príslušnému objektu. Takéto reprezentácie absolútne sa mýlia Jeden objekt môže obsahovať niekoľko vlákien, ktoré vykonávajú rôzne (a niekedy dokonca rovnaké) metódy, pričom správy z objektu sú prenášané a prijímané niekoľkými rôznymi vláknami (mimochodom, toto je jeden z dôvodov, ktoré komplikujú viacvláknové programovanie: Aby ste mohli ladiť program, musíte zistiť, ktoré vlákno v danom okamihu vykonáva ten alebo onen postup!).

Pretože vlákna sú vytvárané z metód objektov, samotný objekt je obvykle vytvorený pred vláknom. Po úspešnom vytvorení objektu program vytvorí vlákno a odovzdá mu adresu metódy objektu a až potom dáva príkaz na spustenie vykonávania vlákna. Procedúra, pre ktorú bolo vlákno vytvorené, môže ako všetky procedúry vytvárať nové objekty, vykonávať operácie s existujúcimi objektmi a volať ďalšie procedúry a funkcie, ktoré sú v jej rozsahu.

Bežné metódy tried je možné vykonávať aj vo vláknach programu. V tomto prípade majte na pamäti aj ďalšiu dôležitú okolnosť: vlákno končí výstupom z postupu, pre ktorý bolo vytvorené. Normálne ukončenie toku programu nie je možné, kým sa procedúra neukončí.

Vlákna sa môžu ukončiť nielen prirodzene, ale aj nenormálne. Spravidla sa to neodporúča. Ďalšie informácie nájdete v časti Ukončenie a prerušenie streamov.

Základné funkcie .NET súvisiace s používaním programových vlákien sú sústredené v priestore názvov Threading. Preto by väčšina viacvláknových programov mala začínať nasledujúcim riadkom:

Importuje systém. Vlákna

Import priestoru názvov uľahčuje písanie programu a umožňuje technológiu IntelliSense.

Priame prepojenie tokov s postupmi naznačuje, že na tomto obrázku delegátov(pozri kapitolu 6). Konkrétne priestor názvov Threading obsahuje delegáta ThreadStart, ktorý sa zvyčajne používa pri spúšťaní vlákien programu. Syntax použitia tohto delegáta vyzerá takto:

Verejný zástupca Sub ThreadStart ()

Kód volaný s delegátom ThreadStart nesmie mať žiadne parametre ani návratovú hodnotu, takže vlákna nemožno vytvárať pre funkcie (ktoré vracajú hodnotu) a pre procedúry s parametrami. Na prenos informácií zo streamu musíte tiež hľadať alternatívne prostriedky, pretože vykonané metódy nevracajú hodnoty a nemôžu používať prenos podľa odkazu. Ak je napríklad ThreadMethod v triede WilluseThread, potom ThreadMethod môže komunikovať informácie úpravou vlastností inštancií triedy WillUseThread.

Aplikačné domény

Vlákna .NET bežia v takzvaných aplikačných doménach, ktoré sú v dokumentácii definované ako „sandbox, v ktorom aplikácia beží“. Doménu aplikácie je možné predstavovať ako odľahčenú verziu procesov Win32; jeden proces Win32 môže obsahovať viacero domén aplikácií. Hlavný rozdiel medzi aplikačnými doménami a procesmi je v tom, že proces Win32 má svoj vlastný adresný priestor (v dokumentácii sa aplikačné domény porovnávajú aj s logickými procesmi bežiacimi vo fyzickom procese). V NET je všetka správa pamäte riešená za behu, takže v jednom procese Win32 môže bežať viac domén aplikácie. Jednou z výhod tejto schémy sú vylepšené možnosti škálovania aplikácií. Nástroje pre prácu s aplikačnými doménami sú v triede AppDomain. Odporúčame vám preštudovať si dokumentáciu k tejto triede. S jeho pomocou môžete získať informácie o prostredí, v ktorom je váš program spustený. Trieda AppDomain sa používa najmä pri vykonávaní reflexie v systémových triedach .NET. Nasledujúci program uvádza načítané zostavy.

Importuje System.Reflection

Modul Modulel

Vedľajšia ()

Dim the Domain As AppDomain

theDomain = AppDomain.CurrentDomain

Dim Assemblies () As

Assemblies = theDomain.GetAssemblies

Dim anAssemblyxAs

Pre každú zostavu v zostavách

Console.WriteLinetanAssembly.Celý názov) Ďalej

Console.ReadLine ()

Koniec pod

Koncový modul

Vytváranie streamov

Začnime rudimentárnym príkladom. Povedzme, že chcete spustiť procedúru v samostatnom vlákne, ktorá zníži hodnotu počítadla v nekonečnej slučke. Postup je definovaný ako súčasť triedy:

Verejná trieda WillUseThreads

Verejné subtractFromCounter ()

Počet dimenzií ako celé číslo

Počítať podľa skutočného počtu - = 1

Console.WriteLlne ("Som v inom vlákne a počítadlo ="

a počítať)

Slučka

Koniec pod

Koncová trieda

Pretože podmienka Do loop je vždy pravdivá, môžete si myslieť, že nič nebude zasahovať do procedúry SubtractFromCounter. Vo viacvláknovej aplikácii to však neplatí vždy.

Nasledujúci úryvok zobrazuje postup Sub Main, ktorý začína vlákno, a príkaz Imports:

Možnosť Strict On Imports System. Modul na vytváranie závitov Modulel

Vedľajšia ()

1 Dim myTest ako nový WillUseThreads ()

2 Dim bThreadStart ako nový ThreadStart (AddressOf _

myTest.SubtractFromCounter)

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

4 "bThread.Start ()

Dim i As Integer

5 Robte, kým je to pravda

Console.WriteLine („V hlavnom vlákne a počet je“ & i) i + = 1

Slučka

Koniec pod

Koncový modul

Pozrime sa na najdôležitejšie body v poradí. V prvom rade postup Sub Man n vždy funguje hlavný prúd(hlavné vlákno). V programoch .NET sú vždy spustené najmenej dve vlákna: hlavné vlákno a vlákno zbierania odpadu. Riadok 1 vytvorí novú inštanciu testovacej triedy. V riadku 2 vytvoríme delegáta ThreadStart a odovzdáme adresu procedúry SubtractFromCounter inštancii testovacej triedy vytvorenej v riadku 1 (tento postup sa volá bez parametrov). DobreImportovaním priestoru názvov Threading môžete dlhý názov vynechať. Nový objekt vlákna je vytvorený na riadku 3. Všimnite si odovzdania delegáta ThreadStart pri volaní konštruktéra triedy Thread. Niektorí programátori dávajú prednosť spojeniu týchto dvoch riadkov do jedného logického riadku:

Dim bThread ako nové vlákno (New ThreadStarttAddressOf _

myTest.SubtractFromCounter))

Nakoniec riadok 4 „spustí“ vlákno zavolaním metódy Start inštancie Thread vytvorenej pre delegáta ThreadStart. Zavolaním tejto metódy hovoríme operačnému systému, že procedúra Odčítanie by mala bežať v samostatnom vlákne.

Slovo „začína“ v predchádzajúcom odseku je uzavreté v úvodzovkách, pretože toto je jedna z mnohých zvláštností viacvláknového programovania: volanie Štart v skutočnosti nezačne vlákno! Hovorí iba operačnému systému, aby naplánoval spustenie určeného vlákna, ale je mimo kontroly programu, aby sa spustil priamo. Samotné spustenie vlákien nebudete môcť začať, pretože spustenie vlákien vždy kontroluje operačný systém. V neskoršej časti sa naučíte, ako použiť prioritu na to, aby operačný systém rýchlejšie spustil vaše vlákno.

Na obr. 10.1 ukazuje príklad toho, čo sa môže stať po spustení programu a jeho následnom prerušení klávesom Ctrl + Break. V našom prípade sa nové vlákno začalo až potom, čo sa počítadlo v hlavnom vlákne zvýšilo na 341!

Ryža. 10.1. Jednoduché viacvláknové spustenie softvéru

Ak program pobeží dlhší čas, výsledok bude vyzerať podobne ako na obr. 10.2. Vidíme, že tydokončenie spusteného vlákna sa pozastaví a kontrola sa opäť prenesie do hlavného vlákna. V tomto prípade existuje prejav preemptívne multithreading prostredníctvom časového krájania. Význam tohto desivého pojmu je vysvetlený nižšie.

Ryža. 10.2. Prepínanie medzi vláknami v jednoduchom viacvláknovom programe

Pri prerušení vlákien a prenose kontroly na iné vlákna používa operačný systém princíp preventívneho viacvláknového spracovania pomocou časového segmentovania. Kvantizácia času rieši aj jeden z bežných problémov, ktoré sa vyskytli predtým vo viacvláknových programoch - jedno vlákno zaberá všetok čas CPU a nie je nižšie ako ovládanie ostatných vlákien (spravidla sa to stáva v intenzívnych cykloch, ako je ten vyššie). Aby sa zabránilo exkluzívnemu únosu procesora, vaše vlákna by mali z času na čas prenášať kontrolu na iné vlákna. Ak sa program ukáže ako „nevedomý“, existuje ďalšie, o niečo menej žiaduce riešenie: operačný systém vždy predbehne spustené vlákno bez ohľadu na úroveň jeho priority, aby malo každé vlákno v systéme prístup k procesoru.

Pretože kvantizačné schémy všetkých verzií systému Windows, ktoré prevádzkujú .NET, majú na každé vlákno alokovaný minimálny časový úsek, v programovaní .NET nie sú problémy s grabovaním vyhradeným pre CPU také vážne. Na druhej strane, ak sa rámec .NET niekedy prispôsobí iným systémom, môže sa to zmeniť.

Ak pred volaním Štart zahrnieme do nášho programu nasledujúci riadok, potom aj vlákna s najnižšou prioritou získajú určitý zlomok času CPU:

bThread.Priority = ThreadPriority.Highest

Ryža. 10.3. Vlákno s najvyššou prioritou sa zvyčajne spustí rýchlejšie

Ryža. 10.4. Procesor je k dispozícii aj pre vlákna s nižšou prioritou

Príkaz priradí novému vláknu maximálnu prioritu a zníži prioritu hlavného vlákna. Obr. 10.3 je možné vidieť, že nové vlákno začína pracovať rýchlejšie ako predtým, ale ako na obr. 10.4, hlavné vlákno tiež dostáva kontrolulenivosť (aj keď veľmi krátku dobu a až po dlhšej práci toku s odčítaním). Keď spustíte program na svojich počítačoch, získate výsledky podobné tým, ktoré sú znázornené na obr. 10.3 a 10.4, ale vzhľadom na rozdiely medzi našimi systémami nebude existovať presná zhoda.

Vymenovaný typ ThreadPrlority obsahuje hodnoty pre päť úrovní priority:

ThreadPriority. Najvyššia

ThreadPriority.AboveNormal

ThreadPrlority. Normálne

ThreadPriority.BlowNormal

ThreadPriority. Najnižšia

Pripojte sa

Niekedy je potrebné vlákno programu pozastaviť, kým sa nedokončí ďalšie vlákno. Povedzme, že chcete pozastaviť vlákno 1, kým vlákno 2 nedokončí svoj výpočet. Pre to z prúdu 1 metóda Pripojenie sa nazýva pre prúd 2. Inými slovami, príkaz

vlákno 2. Pripojiť sa ()

pozastaví aktuálne vlákno a čaká na dokončenie vlákna 2. Vlákno 1 prejde na uzamknutý stav.

Ak sa pripojíte k prúdu 1 k prúdu 2 pomocou metódy Pripojiť, operačný systém automaticky spustí prúd 1 po prúde 2. Všimnite si, že proces spustenia je nedeterministické: nie je možné presne povedať, ako dlho po konci vlákna 2 začne fungovať vlákno 1. Existuje aj iná verzia pripojenia, ktorá vracia booleovskú hodnotu:

vlákno 2. Pripojiť sa (celé číslo)

Táto metóda buď počká, kým sa vlákno 2 dokončí, alebo odblokuje vlákno 1 po uplynutí určeného časového intervalu, čo spôsobí, že plánovač operačného systému znova priradí času CPU vláknu. Metóda vráti hodnotu True, ak sa vlákno 2 skončí pred uplynutím určeného časového limitu, a nepravdivé, ak nie.

Nezabudnite na základné pravidlo: či už sa vlákno 2 dokončilo alebo vypršal časový limit, nemáte žiadnu kontrolu nad tým, kedy je vlákno 1 aktivované.

Názvy vlákien, CurrentThread a ThreadState

Vlastnosť Thread.CurrentThread vracia odkaz na objekt vlákna, ktorý sa práve vykonáva.

Aj keď vo VB .NET existuje nádherné vlákno na ladenie viacvláknových aplikácií, ktoré je popísané nižšie, príkaz nám veľmi často pomohol

MsgBox (Thread.CurrentThread.Name)

Často sa ukázalo, že kód bol spustený v úplne inom vlákne, z ktorého mal byť spustený.

Pripomeňme, že termín „nedeterministické plánovanie tokov programov“ znamená veľmi jednoduchú vec: programátor prakticky nemá k dispozícii žiadne prostriedky, ktoré by ovplyvňovali prácu plánovača. Z tohto dôvodu programy často používajú vlastnosť ThreadState na vrátenie informácií o aktuálnom stave vlákna.

Okno streamov

Okno vlákien v programe Visual Studio .NET je neoceniteľné pri ladení viacvláknových programov. Aktivuje sa príkazom podponuky Debug> Windows v režime prerušenia. Povedzme, že ste vláknu bThread priradili názov pomocou nasledujúceho príkazu:

bThread.Name = "Odčítanie vlákna"

Približný pohľad na okno prúdov po prerušení programu kombináciou klávesov Ctrl + Break (alebo iným spôsobom) je na obr. 10.5.

Ryža. 10.5. Okno streamov

Šípka v prvom stĺpci označuje aktívne vlákno vrátené vlastnosťou Thread.CurrentThread. Stĺpec ID obsahuje číselné ID vlákien. V nasledujúcom stĺpci sú uvedené názvy streamov (ak sú priradené). Stĺpec Poloha označuje postup, ktorý sa má spustiť (napríklad procedúra WriteLine triedy Console na obrázku 10.5). Zostávajúce stĺpce obsahujú informácie o prioritných a pozastavených vláknach (pozri ďalšiu časť).

Okno vlákna (nie operačný systém!) Umožňuje ovládať vlákna vášho programu pomocou kontextových ponúk. Aktuálne vlákno môžete napríklad zastaviť kliknutím pravým tlačidlom myši na príslušný riadok a výberom príkazu Ukotviť (neskôr sa zastavené vlákno dá obnoviť). Zastavenie vlákien sa často používa pri ladení, aby sa zabránilo tomu, že nesprávne fungujúce vlákno bude zasahovať do aplikácie. Okno streamov vám navyše umožňuje aktivovať ďalší (nezastavený) stream; Ak to chcete urobiť, kliknite pravým tlačidlom myši na požadovaný riadok a v kontextovej ponuke vyberte príkaz Prepnúť na vlákno (alebo jednoducho dvakrát kliknite na riadok vlákna). Ako bude ukázané nižšie, je to veľmi užitočné pri diagnostike potenciálnych zablokovaní.

Pozastavenie streamu

Dočasne nevyužité toky je možné preniesť do pasívneho stavu pomocou metódy Slеer. Za blokovaný sa považuje aj pasívny stream. Samozrejme, keď je vlákno uvedené do pasívneho stavu, ostatné vlákna budú mať viac zdrojov procesora. Štandardná syntax metódy Slеer je nasledovná: Thread.Sleep (interval_in_milliseconds)

V dôsledku volania spánku sa aktívne vlákno stane pasívnym aspoň na určitý počet milisekúnd (aktivácia bezprostredne po uplynutí určeného intervalu však nie je zaručená). Poznámka: pri volaní metódy nie je odovzdaný odkaz na konkrétne vlákno - metóda spánku sa volá iba pre aktívne vlákno.

Pri inej verzii režimu spánku sa aktuálne vlákno vzdáva zvyšku vyhradeného času CPU:

Thread.Sleep (0)

Nasledujúca možnosť uvedie aktuálne vlákno do pasívneho stavu na neobmedzený čas (aktivácia nastane iba vtedy, keď zavoláte prerušenie):

Thread.Slеer (Timeout.Infinite)

Pretože pasívne vlákna (aj s neobmedzeným časovým limitom) je možné prerušiť metódou prerušenia, ktorá vo výnimočných prípadoch vedie k spusteniu ThreadlnterruptExcepti, hovor Slayer je vždy uzavretý v bloku Try-Catch, ako v nasledujúcom úryvku:

Skúste

Thread.Sleep (200)

„Pasívny stav vlákna bol prerušený

Chytiť ako výnimku

„Ďalšie výnimky

Koniec pokusu

Každý program .NET beží na programovom vlákne, takže metóda Sleep sa používa aj na pozastavenie programov (ak priestor názvov Threadipg program neimportuje, musíte použiť plne kvalifikovaný názov Threading.Thread. Sleep).

Ukončenie alebo prerušenie vlákien programu

Vlákno sa automaticky ukončí, keď sa vytvorí metóda zadaná pri vytváraní delegáta ThreadStart, ale niekedy sa metóda (a teda aj vlákno) musí ukončiť, keď sa vyskytnú určité faktory. V takýchto prípadoch streamy zvyčajne kontrolujú podmienená premenná, podľa toho v akom staveje rozhodnuté o núdzovom východe z potoka. Do postupu je spravidla zahrnutá slučka Do-While:

Metóda pod vláknom ()

„Program musí poskytnúť prostriedky na prieskum

"podmienená premenná.

„Napríklad podmienenú premennú je možné štylizovať ako vlastnosť

Urobiť while conditionVariable = False And MoreWorkToDo

„Hlavný kód

Loop End Sub

Anketa podmienenej premennej vyžaduje určitý čas. Trvalé hlasovanie v stave cyklu by ste mali používať iba vtedy, ak čakáte na predčasné ukončenie vlákna.

Ak je potrebné premennú podmienky kontrolovať na konkrétnom mieste, použite príkaz If-Then v spojení s Exit Sub vo vnútri nekonečnej slučky.

Prístup k podmienenej premennej musí byť synchronizovaný, aby expozícia z iných vlákien nenarúšala jej bežné používanie. Táto dôležitá téma je popísaná v časti „Riešenie problémov: synchronizácia“.

Kód pasívnych (alebo inak zablokovaných) vlákien sa bohužiaľ nevykonáva, takže voľba s dotazovaním podmienenej premennej pre nich nie je vhodná. V takom prípade zavolajte metódu prerušenia na objektovú premennú, ktorá obsahuje odkaz na požadované vlákno.

Metódu prerušenia je možné zavolať iba na vláknach v stave Čakať, Spať alebo Pripojiť. Ak zavoláte prerušenie pre vlákno, ktoré je v jednom z uvedených stavov, potom vlákno po chvíli začne fungovať znova a prostredie spustenia spustí vlákno ThreadlnterruptedExcepti pri výnimke vo vlákne. K tomu dochádza, aj keď bolo vlákno pasívne neurčené na dobu neurčitú volaním Thread.Sleepdimeout. Nekonečné). Hovoríme „po chvíli“, pretože plánovanie vlákien je nedeterministické. ThreadlnterruptedExcepti na výnimku je zachytený v sekcii Catch obsahujúcej výstupný kód zo stavu čakania. Sekcia Catch však nemusí ukončiť vlákno pri prerušení hovoru - vlákno zvládne výnimku, ako uzná za vhodné.

V .NET môže byť metóda prerušenia volaná aj pre odblokované vlákna. V tomto prípade je vlákno prerušené pri najbližšom blokovaní.

Pozastavenie a zabíjanie nití

Obor názvov Threading obsahuje ďalšie metódy, ktoré prerušujú bežné vytváranie vlákien:

  • Pozastaviť;
  • Prerušiť.

Je ťažké povedať, prečo .NET obsahovala podporu pre tieto metódy - volanie Suspend a Abort pravdepodobne spôsobí, že program bude nestabilný. Žiadna z metód neumožňuje normálnu deinicializáciu toku. Navyše pri volaní Pozastaviť alebo Prerušiť nie je možné predpovedať, v akom stave vlákno zanechá objekty po pozastavení alebo prerušení.

Volanie Abort vyvolá výnimku ThreadAbortException. Aby sme vám pomohli pochopiť, prečo by sa s touto podivnou výnimkou nemalo zaobchádzať v programoch, uvádzame výňatok z dokumentácie .NET SDK:

„... Keď je vlákno zničené volaním Abort, runtime vyvolá ThreadAbortException. Toto je špeciálny druh výnimky, ktorý program nemôže zachytiť. Keď je vyvolaná táto výnimka, runtime spustí všetky bloky Nakoniec pred ukončením vlákna. Pretože v blokoch Nakoniec je možné vykonať akúkoľvek akciu, zavolajte na Pripojiť sa a ubezpečte sa, že stream je zničený. "

Morálne: prerušenie a pozastavenie sa neodporúča (a ak sa napriek tomu bez pozastavenia nezaobídete, pokračujte v pozastavenom vlákne pomocou metódy Pokračovať). Vlákno môžete bezpečne ukončiť iba pomocou pollingu synchronizovanej premennej podmienky alebo zavolaním vyššie uvedenej metódy prerušenia.

Vlákna na pozadí (démoni)

Niektoré vlákna spustené na pozadí sa automaticky zastavia, keď sa zastavia ostatné súčasti programu. Najmä smetiar beží v jednom z vlákien na pozadí. Vlákna na pozadí sa zvyčajne vytvárajú na príjem údajov, ale to sa robí iba vtedy, ak v iných vláknach beží kód, ktorý dokáže spracovať prijaté údaje. Syntax: názov streamu. IsBackGround = True

Ak v aplikácii zostanú iba vlákna na pozadí, aplikácia sa automaticky ukončí.

Vážnejší príklad: extrahovanie údajov z kódu HTML

Streamy odporúčame používať iba vtedy, ak je funkčnosť programu jasne rozdelená na niekoľko operácií. Dobrým príkladom je program na extrakciu HTML v kapitole 9. Naša trieda robí dve veci: načítanie údajov z Amazonu a ich spracovanie. Je to perfektný príklad situácie, v ktorej je viacvláknové programovanie skutočne vhodné. Vytvárame triedy pre niekoľko rôznych kníh a potom analyzujeme údaje v rôznych prúdoch. Vytvorenie nového vlákna pre každú knihu zvyšuje efektivitu programu, pretože zatiaľ čo jedno vlákno prijíma údaje (čo môže vyžadovať čakanie na serveri Amazon), iné vlákno bude zaneprázdnené spracovaním údajov, ktoré už boli prijaté.

Viacvláknová verzia tohto programu funguje efektívnejšie ako verzia s jedným vláknom iba na počítači s niekoľkými procesormi alebo ak je možné príjem ďalších údajov efektívne kombinovať s ich analýzou.

Ako bolo uvedené vyššie, vo vláknach je možné spustiť iba procedúry, ktoré nemajú žiadne parametre, takže budete musieť v programe vykonať menšie zmeny. Nasleduje základný postup, prepísaný tak, aby sa vylúčili parametre:

Verejné hodnotenie Sub Find

m_Rank = ScrapeAmazon ()

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

Koniec pod

Pretože kombinované pole nebudeme môcť používať na ukladanie a získavanie informácií (písanie viacvláknových programov s grafickým rozhraním je popísané v poslednej časti tejto kapitoly), program ukladá údaje štyroch kníh do poľa, definícia ktorej začína takto:

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

theBook (0.l) = "Programovanie VB .NET" "Atď.

V rovnakom cykle, v ktorom sa vytvárajú objekty AmazonRanker, sa vytvoria štyri streamy:

Pre i = 0 až 3

Skúste

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

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

aThread = Nové vlákno (aThreadStart)

aThread.Name = kniha (i.l)

aThread.Start () Chytiť e ako výnimku

Console.WriteLine (e.Message)

Koniec pokusu

Ďalšie

Nasleduje kompletný text programu:

Možnosť Strict On Imports System.IO Imports System.Net

Importuje systém. Vlákna

Modul Modulel

Vedľajšia ()

Dim theBook (3.1) as String

kniha (0,0) = "1893115992"

theBook (0.l) = "Programovanie VB .NET"

kniha (l.0) = "1893115291"

theBook (l.l) = "Programovanie databázy VB .NET"

kniha (2,0) = "1893115623"

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

kniha (3.0) = "1893115593"

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

Dim i As Integer

Dim theRanker As = AmazonRanker

Dim aThreadStart stmaviť ako Threading.ThreadStart

Dim aThread as Threading.Thread

Pre i = 0 až 3

Skúste

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

aThreadStart = Nový ThreadStart (adresa na hodnotenie. FindRank)

aThread = Nové vlákno (aThreadStart)

aThread.Name = kniha (i.l)

aThread.Start ()

Chytiť ako výnimku

Console.WriteLlnete.Message)

Koniec Skúste Ďalej

Console.ReadLine ()

Koniec pod

Koncový modul

Verejná trieda AmazonRanker

Súkromná m_URL ako reťazec

Súkromné ​​m_Rank ako celé číslo

Súkromné ​​m_Name ako reťazec

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

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

m_Name = koncový podnázov názvu

Public Sub FindRank () m_Rank = ScrapeAmazon ()

Console.Writeline ("hodnosť" & m_Name & "je"

& GetRank) End Sub

Verejné vlastníctvo určené len na čítanie GetRank () ako reťazec

Ak m_Rank<>0 Potom

Vrátiť CStr (m_Rank) Inak

„Problémy

Koniec Ak

End Get

Koncová nehnuteľnosť

Verejné vlastníctvo určené len na čítanie GetName () ako reťazec Získať

Vráťte m_Name

End Get

Koncová nehnuteľnosť

Súkromná funkcia ScrapeAmazon () ako 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 (údaje)

Chytiť E ako výnimku

Console.WriteLine (E.Message)

Console.WriteLine (E.StackTrace)

Konzola. ReadLine ()

Funkcia End Try End

Analýza súkromných funkcií (ByVal theData ako reťazec) ako celé číslo

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

Poradie predaja:") _

+ "Poradie predaja na Amazon.com:„.Dĺžka

Dim temp As String

Urobiť, kým theData.Substring (Location.l) = "<" temp = temp

& theData.Substring (Location.l) Poloha + = 1 slučka

Vrátiť Clnt (teplota)

Koncová funkcia

Koncová trieda

V menných priestoroch .NET a I / O sa bežne používajú viacvláknové operácie, takže knižnica .NET Framework pre ne poskytuje špeciálne asynchrónne metódy. Ďalšie informácie o použití asynchrónnych metód pri písaní viacvláknových programov nájdete v metódach BeginGetResponse a EndGetResponse triedy HTTPWebRequest.

Hlavné nebezpečenstvo (všeobecné údaje)

Doteraz sa zvažoval jediný bezpečný prípad použitia nití - naše streamy nezmenili všeobecné údaje. Ak povolíte zmenu všeobecných údajov, potenciálne chyby sa začnú exponenciálne množiť a je oveľa ťažšie ich zbaviť pre program. Na druhej strane, ak zakážete úpravu zdieľaných údajov rôznymi vláknami, viacvláknové programovanie .NET sa bude len málo líšiť od obmedzených možností VB6.

Ponúkame vám malý program, ktorý demonštruje problémy, ktoré vznikajú, bez toho, aby zachádzal do zbytočných podrobností. Tento program simuluje dom s termostatom v každej miestnosti. Ak je teplota o 5 stupňov Fahrenheita alebo viac (asi 2,77 stupňa Celzia) nižšia ako cieľová teplota, nariadime vykurovaciemu systému, aby zvýšil teplotu o 5 stupňov; v opačnom prípade teplota stúpne iba o 1 stupeň. Ak je aktuálna teplota väčšia alebo rovná nastavenej, nevykonáva sa žiadna zmena. Regulácia teploty v každej miestnosti sa vykonáva so samostatným prietokom s 200-milisekundovým oneskorením. Hlavná práca sa vykonáva pomocou nasledujúceho úryvku:

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

Thread.Sleep (200)

Catch tie As ThreadlnterruptedException

„Pasívne čakanie bolo prerušené

Chytiť ako výnimku

„Iné výnimky z pokusu o koniec

mHouse.HouseTemp + - 5 "atď.

Nasleduje kompletný zdrojový kód programu. Výsledok je znázornený na obr. 10.6: Teplota v dome dosiahla 105 stupňov Fahrenheita (40,5 stupňa Celzia)!

1 Možnosť Strict On

2 Importuje systém. Vlákna

3 Modulový modul

4 Sub Main ()

5 Dim myHouse As New House (l0)

6 Konzola. ReadLine ()

7 Koniec podč

8 koncový modul

9 Dom verejnej triedy

10 Public Const MAX_TEMP As Integer = 75

11 Súkromný mCurTemp ako celé číslo = 55

12 súkromných mRooms () ako izba

13 Verejná čiastková novinka (ByVal numOfRooms ako celé číslo)

14 miestností ReDim mRooms (numOfRooms = 1)

15 Dim i As Integer

16 Dim aThreadStart stlmte ako navliekanie. ThreadStart

17 Dim aVlákno ako vlákno

18 Pre i = 0 až numOfRooms -1

19 Skúste

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

21 aThreadStart - nový ThreadStart (adresa _

mRooms (i) .CheckTempInRoom)

22 aThread = Nové vlákno (aThreadStart)

23 aVlákno. Začiatok ()

24 Úlovok E ako výnimka

25 Console.WriteLine (E.StackTrace)

26 Koniec pokusu

27 Ďalej

28 Koniec podč

29 Verejný majetok HouseTemp () ako celé číslo

tridsať. Dostať

31 Návrat mCurTemp

32 Koniec získať

33 Set (hodnota ByVal ako celé číslo)

34 mCurTemp = hodnota 35 koncová sada

36 Koncová nehnuteľnosť

37 Koncová trieda

38 Miestnosť pre verejnú triedu

39 Súkromné ​​mCurTemp ako celé číslo

40 Súkromné ​​mName As String

41 Súkromný mHouse As House

42 Public Sub New (ByVal theHouse As House,

ByVal temp As Integer, ByVal roomName As String)

43 mDom = dom

44 mCurTemp = teplota

45 mName = nazov miestnosti

46 Koniec podč

47 Verejná čiastková kontrolaTempInRoom ()

48 Zmena teploty ()

49 Koniec podč

50 súkromných čiastkových teplôt zmeny ()

51 Skúste

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

53 vlákien. Spánok (200)

54 mDom. Teplota domu + - 5

55 Console.WriteLine („Som v“ & Me.mName & _

56 ". Aktuálna teplota je" & mHouse.HouseTemp)

57. Sám mHouse.HouseTemp< mHouse.MAX_TEMP Then

58 vlákien. Spánok (200)

59 mHouse.HouseTemp + = 1

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

61 ". Aktuálna teplota je" & mHouse.HouseTemp)

62 Inak

63 Console.WriteLine („Som v“ & Me.mName & _

64 ". Aktuálna teplota je" & mHouse.HouseTemp)

65 "Nerobte nič, teplota je normálna

66 Koniec Ak

67 Catch tae As ThreadlnterruptedException

68 "Pasívne čakanie bolo prerušené

69 Chytiť ako výnimku

70 "Ďalšie výnimky

71 Koniec pokusu

72 End Sub

73 Koncová trieda

Ryža. 10.6. Viacvláknové problémy

Procedúra Sub Main (riadky 4-7) vytvára „dom“ s desiatimi „miestnosťami“. Trieda House nastavuje maximálnu teplotu 75 stupňov Fahrenheita (asi 24 stupňov Celzia). Riadky 13-28 definujú pomerne zložitého staviteľa domu. Riadky 18-27 sú kľúčové pre pochopenie programu. Riadok 20 vytvára ďalší objekt miestnosti a odkaz na objekt domu je postúpený konštruktérovi, aby sa naň objekt miestnosti mohol v prípade potreby odvolať. Riadky 21-23 štartujú desať prúdov na úpravu teploty v každej miestnosti. Trieda Room je definovaná na riadkoch 38-73. Referencia domu coxpaje uložený v premennej mHouse v konštruktore triedy Room (riadok 43). Kód na kontrolu a úpravu teploty (riadky 50-66) vyzerá jednoducho a prirodzene, ale ako čoskoro uvidíte, tento dojem klame! Všimnite si toho, že tento kód je zabalený do bloku Try-Catch, pretože program používa metódu Sleep.

Málokto by súhlasil s tým, aby žil v teplotách 40,5 až 24 stupňov Celzia (105 stupňov Fahrenheita). Čo sa stalo? Problém súvisí s nasledujúcim riadkom:

Ak mHouse.HouseTemp< mHouse.MAX_TEMP - 5 Then

A stane sa nasledovné: najskôr sa teplota skontroluje prietokom 1. Vidí, že teplota je príliš nízka, a zvýši ju o 5 stupňov. Nanešťastie, skôr ako teplota stúpne, prúd 1 sa preruší a riadenie sa prenesie do prúdu 2. Tok 2 kontroluje rovnakú premennú ako ešte nebolo zmenené prietok 1. Tok 2 sa teda tiež pripravuje na zvýšenie teploty o 5 stupňov, ale nemá na to čas a tiež prechádza do stavu čakania. Tento proces pokračuje, kým sa prúd 1 neaktivuje a neprejde na nasledujúci príkaz - zvýšenie teploty o 5 stupňov. Nárast sa opakuje, keď je aktivovaných všetkých 10 prúdov a obyvatelia domu budú mať zlé obdobie.

Riešenie problému: synchronizácia

V predchádzajúcom programe nastáva situácia, keď výstup programu závisí od poradia vykonávania vlákien. Aby ste sa toho zbavili, musíte sa uistiť, že príkazy ako

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

sú aktívne spracované aktívnym vláknom pred jeho prerušením. Táto vlastnosť sa nazýva atómová hanba - blok kódu musí byť vykonaný každým vláknom bez prerušenia ako atómová jednotka. Skupinu príkazov skombinovaných do atómového bloku nemôže plánovač vlákien prerušiť, kým nie je dokončený. Každý viacvláknový programovací jazyk má svoje vlastné spôsoby zabezpečenia atomicity. Vo VB .NET je najľahším spôsobom použitia príkazu SyncLock odovzdanie objektovej premennej pri volaní. Vykonajte malé zmeny v postupe ChangeTemperature z predchádzajúceho príkladu a program bude fungovať správne:

Private Sub ChangeTemperature () SyncLock (mHouse)

Skúste

Ak mHouse.HouseTemp< mHouse.MAXJTEMP -5 Then

Thread.Sleep (200)

mHouse.HouseTemp + = 5

Console.WriteLine („Som v“ a Me.mName & _

„. Aktuálna teplota je“ & mHouse.HouseTemp)

Ja sám

mHouse.HouseTemp< mHouse. MAX_TEMP Then

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

Console.WriteLine ("Am in" & Me.mName & _ ". Aktuálna teplota je" & mHouse.HomeTemp) Inak

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

„Nerobte nič, teplota je normálna

End If Catch tie As ThreadlnterruptedException

„Pasívne čakanie prerušila výnimka Catch e As

„Ďalšie výnimky

Koniec pokusu

Ukončite SyncLock

Koniec pod

Blokový kód SyncLock sa vykonáva atómovo. Prístup k nemu zo všetkých ostatných vlákien bude zatvorený, kým prvé vlákno neuvoľní zámok príkazom End SyncLock. Ak vlákno v synchronizovanom bloku prejde do stavu pasívneho čakania, zámok zostane, kým sa vlákno nepreruší alebo neobnoví.

Správne používanie príkazu SyncLock zaistí bezpečnosť vlákna vášho programu. Nadmerné používanie SyncLock má bohužiaľ negatívny vplyv na výkon. Synchronizácia kódu vo viacvláknovom programe niekoľkokrát zníži rýchlosť jeho práce. Synchronizujte iba najnutnejší kód a uvoľnite zámok čo najskôr.

Základné triedy kolekcie nie sú bezpečné vo viacvláknových aplikáciách, ale .NET Framework obsahuje verzie väčšiny tried kolekcie bezpečné pre vlákna. V týchto triedach je kód potenciálne nebezpečných metód uzavretý v blokoch SyncLock. Verzie zberných tried bezpečné pre vlákna by sa mali používať vo viacvláknových programoch všade tam, kde je ohrozená integrita údajov.

Zostáva spomenúť, že podmienené premenné sa dajú ľahko implementovať pomocou príkazu SyncLock. Na to stačí synchronizovať zápis do spoločnej boolovskej vlastnosti, ktorá je k dispozícii na čítanie a zápis, ako sa to robí v nasledujúcom fragmente:

Verejná podmienka triedyVariabilná

Súkromná zdieľaná skrinka ako objekt = nový objekt ()

Private Shared mOK As Boolean Shared

Vlastníctvo TheConditionVariable () ako booleovský

Dostať

Vrátiť mOK

End Get

Nastaviť (ByVal Value As Boolean) SyncLock (skrinka)

mOK = hodnota

Ukončite SyncLock

Koncová sada

Koncová nehnuteľnosť

Koncová trieda

Trieda príkazov a monitorov SyncLock

Použitie príkazu SyncLock zahŕňa niektoré jemnosti, ktoré neboli uvedené v jednoduchých príkladoch vyššie. Výber synchronizačného objektu teda hrá veľmi dôležitú úlohu. Skúste spustiť predchádzajúci program pomocou príkazu SyncLock (Me) namiesto SyncLock (mHouse). Teplota opäť vystúpi nad prah!

Nezabudnite, že príkaz SyncLock sa synchronizuje pomocou predmet, odovzdané ako parameter, nie útržkom kódu. Parameter SyncLock funguje ako brána pre prístup k synchronizovanému fragmentu z iných vlákien. Príkaz SyncLock (Me) skutočne otvára niekoľko rôznych „dverí“, čo je presne to, čomu ste sa pri synchronizácii pokúšali vyhnúť. Morálka:

Na ochranu zdieľaných údajov vo viacvláknovej aplikácii musí príkaz SyncLock synchronizovať jeden objekt naraz.

Pretože je synchronizácia spojená s konkrétnym objektom, v niektorých situáciách je možné nechtiac uzamknúť ďalšie fragmenty. Povedzme, že máte dve synchronizované metódy, prvú a druhú a obe metódy sú synchronizované s objektom bigLock. Keď vlákno 1 zadá metódu ako prvé a zachytí bigLock, žiadne vlákno nebude môcť zadať metódu druhú, pretože prístup k nej je už obmedzený na vlákno 1!

Funkčnosť príkazu SyncLock je možné považovať za podmnožinu funkcií triedy Monitor. Trieda Monitor je vysoko prispôsobiteľná a môže byť použitá na riešenie netriviálnych synchronizačných úloh. Príkaz SyncLock je približným analógom metód Enter a Exit z triedy Moni tor:

Skúste

Monitor.Enter (theObject) Nakoniec

Monitor.Exit (theObject)

Koniec pokusu

Pre niektoré štandardné operácie (zvýšenie / zníženie premennej, výmena obsahu dvoch premenných) .NET Framework poskytuje triedu Interlocked, ktorej metódy vykonávajú tieto operácie na atómovej úrovni. Pri použití triedy Interlocked sú tieto operácie oveľa rýchlejšie ako pomocou príkazu SyncLock.

Zámkové

Pri synchronizácii je zámok nastavený na objekty, nie na vlákna, takže pri použití rôzne blokovať objekty rôzneútržky kódu v programoch sa niekedy vyskytujú celkom netriviálne chyby. Bohužiaľ, v mnohých prípadoch synchronizácia na jednom objekte jednoducho nie je povolená, pretože to povedie k príliš častému blokovaniu vlákien.

Zvážte situáciu zámkové(zablokovanie) v najjednoduchšej forme. Predstavte si dvoch programátorov pri večernom stole. Bohužiaľ majú iba jeden nôž a jednu vidličku pre dvoch. Za predpokladu, že na jedlo potrebujete nôž aj vidličku, sú možné dve situácie:

  • Jednému programátorovi sa podarí chytiť nôž a vidličku a začne jesť. Keď sa nasýti, odloží večeru bokom a potom ich môže vziať ďalší programátor.
  • Jeden programátor vezme nôž a druhý vidličku. Ani jeden nemôže začať jesť, pokiaľ sa ten druhý nevzdá svojho spotrebiča.

Vo viacvláknovom programe sa táto situácia nazýva vzájomné blokovanie. Tieto dve metódy sú synchronizované s rôznymi objektmi. Vlákno A zachytáva objekt 1 a vstupuje do programovej časti chránenej týmto objektom. Na to, aby fungoval, potrebuje prístup k kódu chránenému iným synchronizačným zámkom s iným synchronizačným objektom. Kým však stihnete zadať fragment, ktorý je synchronizovaný iným objektom, prúd B do neho vstúpi a zachytí tento objekt. Vlákno A teraz nemôže vstúpiť do druhého fragmentu, vlákno B nemôže vstúpiť do prvého fragmentu a obe vlákna sú odsúdené na neobmedzené čakanie. Žiadne vlákno nemôže pokračovať v prevádzke, pretože požadovaný objekt nebude nikdy uvoľnený.

Diagnóza zablokovania je komplikovaná skutočnosťou, že sa môžu vyskytnúť v relatívne zriedkavých prípadoch. Všetko závisí od poradia, v akom im plánovač pridelí čas CPU. Je možné, že vo väčšine prípadov budú synchronizačné objekty zachytené v poradí bez zablokovania.

Nasleduje implementácia situácie zablokovania, ktorá bola práve popísaná. Po krátkej diskusii o najzákladnejších bodoch si ukážeme, ako identifikovať zablokovanú situáciu v okne vlákna:

1 Možnosť Strict On

2 Importuje systém. Vlákna

3 Modulový modul

4 Sub Main ()

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

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

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

8 Dim aThread ako nové vlákno (aThreadStart)

9 aThread.Name = "Tom"

10 Dim bThreadStart ako nový ThreadStarttAddressOf Bob.Eat)

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

12 bThread.Name = "Bob"

13 aVlákno. Začiatok ()

14 bThread.Start ()

15 Koniec podč

16 koncový modul

17 Vidlica verejnej triedy

18 súkromných zdieľaných mForkAvaiTable ako booleovský = pravda

19 Súkromný zdieľaný vlastník ako reťazec = „Nikto“

20 Súkromný majetok len na čítanie OwnsUtensil () ako reťazec

21 Získajte

22 Spätný majiteľ

23 Koniec získať

24 Koncová nehnuteľnosť

25 Public Sub GrabForktByVal a As Programmer)

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

„pokúšam sa chytiť vidličku.“)

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

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

29 Ak je mForkAvailable Potom

30 a.HasFork = Pravda

31 vlastník = a.MôjMeno

32 mForkAvailable = Falošné

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

34 Skúste

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

Koniec pokusu

35 Koniec Ak

36 Monitor. Ukončiť (ja)

Ukončite SyncLock

37 Koniec podč

38 Koncová trieda

39 Nôž verejnej triedy

40 Súkromný zdieľaný mKnifeK dispozícii ako booleovský = pravda

41 súkromný zdieľaný vlastník ako reťazec = „nikto“

42 Súkromný majetok iba na čítanie, vlastníctvo Utensi1 () ako reťazec

43 Získajte

44 Spätný majiteľ

45 Koniec získať

46 Koncová nehnuteľnosť

47 Public Sub GrabKnifetByVal ako programátor)

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

„pokúšam sa chytiť nôž.“)

49 Console.WriteLine (Me.OwnsUtensil & "has the knife.")

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

51 Ak je mKnifeAvailable Then

52 mKnifeAvailable = Nepravda

53 a.HasKnife = Pravda

54 vlastník = a.MôjMeno

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

56 Skúste

Thread.Sleep (100)

Chytiť ako výnimku

Console.WriteLine (e.StackTrace)

Koniec pokusu

57 Koniec Ak

58 Monitor. Ukončiť (ja)

59 Koniec podč

60 Koncová trieda

61 Programátor verejnej triedy

62 Súkromné ​​mName As String

63 Private Shared mFork As Fork

64 Súkromný spoločný nôž ako nôž

65 Súkromný mHasKnife ako booleovský

66 Súkromný mHasFork ako booleovský

67 Shared Sub New ()

68 mFork = nová vidlica ()

69 mKnife = nový nôž ()

70 End Sub

71 Public Sub New (ByVal theName As String)

72 mName = theName

73 Koniec podč

74 Verejné vlastníctvo určené len na čítanie MyName () ako reťazec

75 Získajte

76 Vráťte sa mName

77 Koniec získať

78 Koncová nehnuteľnosť

79 Verejný majetok HasKnife () ako booleovský

80 Získajte

81 Vráťte mHasKnife

82 Koniec získať

83 Set (ByVal Value As Boolean)

84 mHasKnife = hodnota

85 Koncová sada

86 Koncová nehnuteľnosť

87 Verejný majetok HasFork () ako booleovský

88 Získajte

89 Návrat mHasFork

90 End Get

91 Set (ByVal Value As Boolean)

92 mHasFork = hodnota

93 Koncová sada

94 Koncová nehnuteľnosť

95 Verejné podjedlo ()

96 Do Do Me.HasKnife And Me.HasFork

97 Console.Writeline (Thread.CurrentThread.Name & "je vo vlákne.")

98 Ak Rnd ()< 0.5 Then

99 mFork.GrabFork (ja)

100 inak

101 mKnife.GrabKnife (ja)

102 Koniec Ak

103 Slučka

104 MsgBox (Me.MyName & „môžem jesť!“)

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

106 mVidlica = nová vidlica ()

107 Koniec podč

108 Koncová trieda

Hlavný postup Main (riadky 4-16) vytvorí dve inštancie triedy Programmer a potom spustí dve vlákna na vykonanie kritickej metódy Eat triedy Programmer (riadky 95-108), popísanej nižšie. Hlavný postup nastaví názvy vlákien a nastaví ich; asi všetko, čo sa deje, je zrozumiteľné a bez komentára.

Kód pre triedu Fork vyzerá zaujímavejšie (riadky 17-38) (podobná trieda Knife je definovaná v riadkoch 39-60). Riadky 18 a 19 určujú hodnoty spoločných polí, pomocou ktorých môžete zistiť, či je zásuvka momentálne k dispozícii, a ak nie, kto ju používa. Vlastnosť ReadOnly OwnUtensi1 (riadky 20-24) je určená na najjednoduchší prenos informácií. Ústredným prvkom triedy Fork je metóda GrabFork „chyť vidličku“ definovaná v riadkoch 25-27.

  1. Riadky 26 a 27 jednoducho vytlačia informácie o ladení do konzoly. V hlavnom kóde metódy (riadky 28-36) je prístup k vidlici synchronizovaný objektomopasok Me. Pretože náš program používa iba jednu vidlicu, Me sync zaisťuje, že ho nemôžu zachytiť žiadne dve vlákna súčasne. Príkaz Slee "p (v bloku začínajúcom na riadku 34) simuluje oneskorenie medzi uchopením vidličky / noža a začiatkom jedla. Všimnite si, že príkaz Spánok neodomkne predmety a iba urýchli zablokovanie!
    Najzaujímavejší je však kód triedy Programmer (riadky 61-108). Riadky 67-70 definujú generický konštruktor, ktorý zabezpečí, aby bola v programe iba jedna vidlička a nôž. Kód vlastnosti (riadky 74-94) je jednoduchý a nevyžaduje žiadny komentár. To najdôležitejšie sa deje v metóde Eat, ktorá sa vykonáva pomocou dvoch samostatných vlákien. Proces pokračuje v slučke, kým nejaký prúd nezachytí vidličku spolu s nožom. Na linkách 98-102 objekt náhodne chytí vidličku / nôž pomocou Rnd hovoru, čo spôsobuje zablokovanie. Stáva sa nasledovné:
    Vyvolá sa vlákno, ktoré vykonáva metódu Eat z Tot, a vstúpi do slučky. Chytí nôž a prejde do stavu čakania.
  2. Vyvolá sa vlákno vykonávajúce Bobovu jesť metódu a vstúpi do slučky. Nemôže uchopiť nôž, ale uchopí vidličku a prejde do pohotovostného stavu.
  3. Vyvolá sa vlákno, ktoré vykonáva metódu Eat z Tot, a vstúpi do slučky. Snaží sa chytiť vidličku, ale Bob už chytil vidličku; vlákno prejde do stavu čakania.
  4. Vyvolá sa vlákno vykonávajúce Bobovu jesť metódu a vstúpi do slučky. Snaží sa chytiť nôž, ale nôž je už zajatý predmetom Thoth; vlákno prejde do stavu čakania.

To všetko pokračuje donekonečna - stojíme pred typickou situáciou slepej uličky (skúste spustiť program a uvidíte, že nikto nemôže takto jesť).
V okne vlákien môžete tiež skontrolovať, či nedošlo k zablokovaniu. Spustite program a prerušte ho pomocou klávesov Ctrl + Break. Zahrňte do výrezu premennú Ja a otvorte okno streamov. Výsledok vyzerá niečo ako ten, ktorý je znázornený na obr. 10.7. Z obrázku je vidieť, že Bobova niť chytila ​​nôž, ale nemá vidličku. Kliknite pravým tlačidlom myši v okne Vlákna na riadok Tot a z kontextovej ponuky vyberte príkaz Prepnúť na vlákno. Výhľad ukazuje, že potok Thoth má vidličku, ale žiadny nôž. Toto samozrejme nie je stopercentný dôkaz, ale pri takom správaní máte prinajmenšom podozrenie, že niečo nie je v poriadku.
Ak nie je možná možnosť synchronizácie pomocou jedného objektu (ako v programe so zvýšením teploty v dome), aby ste predišli vzájomným zámkom, môžete synchronizačné objekty očíslovať a vždy ich zachytiť v konštantnom poradí. Pokračujme analógiou programátora stolovania: ak niť vždy vezme najskôr nôž a potom vidličku, nebudú problémy so zaistením. Prvý prúd, ktorý chytí nôž, bude môcť normálne jesť. V preklade do jazyka programových tokov to znamená, že zachytenie objektu 2 je možné iba vtedy, ak je objekt 1 zachytený ako prvý.

Ryža. 10.7. Analýza zablokovania v okne vlákna

Ak teda odstránime hovor na Rnd na linke 98 a nahradíme ho útržkom

mFork.GrabFork (ja)

mKnife.GrabKnife (ja)

slepá ulička zmizne!

Spolupracujte na údajoch pri ich vytváraní

Vo viacvláknových aplikáciách často dochádza k situácii, že vlákna nielen pracujú so zdieľanými údajmi, ale aj čakajú, kým sa objavia (to znamená, že vlákno 1 musí vytvoriť údaje, než ho vlákno 2 bude môcť použiť). Pretože sú údaje zdieľané, prístup k nim je potrebné synchronizovať. Je tiež potrebné poskytnúť prostriedky na informovanie čakajúcich vlákien o vzhľade pripravených údajov.

Táto situácia sa zvyčajne nazýva problém dodávateľa / spotrebiteľa. Vlákno sa pokúša získať prístup k údajom, ktoré ešte neexistujú, takže musí preniesť riadenie do iného vlákna, ktoré vytvára požadované údaje. Problém je vyriešený nasledujúcim kódom:

  • Vlákno 1 (spotrebiteľ) sa prebudí, zadá synchronizovanú metódu, vyhľadá údaje, nenájde ich a prejde do stavu čakania. Predbežnefyzicky musí odstrániť blokovanie, aby nezasahovalo do práce dodávajúceho vlákna.
  • Vlákno 2 (poskytovateľ) zadáva synchronizovanú metódu uvoľnenú vláknom 1, vytváraúdaje pre stream 1 a nejakým spôsobom upozorní stream 1 o prítomnosti údajov. Potom uvoľní zámok, aby vlákno 1 mohlo spracovať nové údaje.

Nepokúšajte sa tento problém vyriešiť neustálym vyvolávaním vlákna 1 a kontrolou stavu premennej podmienky, ktorej hodnota je> nastavená vláknom 2. Toto rozhodnutie vážne ovplyvní výkon vášho programu, pretože vo väčšine prípadov sa vlákno 1 bude vyvolávať nie. dôvod; a vlákno 2 bude čakať tak často, že mu dôjde čas na vytvorenie údajov.

Vzťahy medzi poskytovateľom a spotrebiteľom sú veľmi časté, preto sú pre takéto situácie vo viacvláknových knižniciach programovacích tried vytvorené špeciálne primitíva. V NET sa tieto primitívy nazývajú Wait a Pulse-PulseAl 1 a sú súčasťou triedy Monitor. Obrázok 10.8 ilustruje situáciu, ktorú sa chystáme naprogramovať. Program organizuje tri fronty vlákien: front na čakanie, blokovací front a front na spustenie. Plánovač vlákien neprideľuje čas CPU vláknam, ktoré sú vo fronte na čakanie. Aby mal vlákno pridelený čas, musí sa presunúť do frontu na spustenie. Výsledkom je, že práca s aplikáciou je organizovaná oveľa efektívnejšie ako pri bežnom dotazovaní podmienenej premennej.

V pseudokode je fráza spotrebiteľa údajov formulovaná nasledovne:

"Vstup do synchronizovaného bloku nasledujúceho typu

Aj keď žiadne údaje

Prejdite do čakacej fronty

Slučka

Ak existujú údaje, spracujte ich.

Opustiť synchronizovaný blok

Ihneď po vykonaní príkazu Wait sa vlákno pozastaví, zámok sa uvoľní a vlákno vstúpi do čakajúceho radu. Keď sa zámok uvoľní, vlákno vo fronte spustenia sa môže spustiť. V priebehu času jedno alebo viac blokovaných vlákien vytvorí údaje potrebné na prevádzku vlákna, ktoré je vo fronte na čakanie. Pretože sa validácia údajov vykonáva v slučke, prechod na používanie údajov (za slučkou) nastane, iba ak sú údaje pripravené na spracovanie.

V pseudokóde vyzerá fráza poskytovateľa údajov takto:

"Zadanie bloku synchronizovaného zobrazenia

Aj keď údaje NIE sú potrebné

Prejdite do čakacej fronty

Ostatné údaje o produkcii

Keď sú údaje pripravené, zavolajte na Pulse-PulseAll.

presunúť jedno alebo viac vlákien z frontu blokovania do frontu na spustenie. Opustite synchronizovaný blok (a vráťte sa do frontu na spustenie)

Predpokladajme, že náš program simuluje rodinu s jedným rodičom, ktorý zarába peniaze, a dieťaťom, ktoré tieto peniaze míňa. Keď peniaze skončiaukazuje sa, že dieťa musí čakať na príchod novej sumy. Softvérová implementácia tohto modelu vyzerá takto:

1 Možnosť Strict On

2 Importuje systém. Vlákna

3 Modulový modul

4 Sub Main ()

5 Dim the Family as New Family ()

6 theFamily.StartltsLife ()

7 Koniec podč

8 Koncový fjodule

9

10 Rodina verejnej triedy

11 Súkromné ​​peňažné peniaze ako celé číslo

12 Súkromný mTýždeň ako celé číslo = 1

13 verejných čiastkových životov ()

14 Dim aThreadStart as New ThreadStarUAddressOf Me.Produce)

15 Dim bThreadStart ako nový ThreadStarUAddressOf Me.Consume)

16 Dim aThread as new Thread (aThreadStart)

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

18 aThread.Name = "Produkovať"

19 aVlákno. Začiatok ()

20 bThread.Name = "Konzumovať"

21 bVlákno. Štart ()

22 Koniec podč

23 Verejný majetok TheWeek () ako celé číslo

24 Získajte

25 Vráťte sa za týždeň

26 Koniec získať

27 Set (hodnota ByVal ako celé číslo)

28 týždňov - hodnota

29 Koncová sada

30 Koncová nehnuteľnosť

31 Verejný majetok OurMoney () ako celé číslo

32 Získajte

33 Vráťte peniaze

34 Koniec získať

35 Set (hodnota ByVal ako celé číslo)

36 mMoney = hodnota

37 Koncová sada

38 Koncová nehnuteľnosť

39 Verejná subprodukcia ()

40 vlákien. Spánok (500)

41 Do

42 Monitor. Zadajte (ja)

43 Do While Me.OurMoney> 0

44 Monitor. Počkajte (ja)

45 slučka

46 me. Naše peniaze = 1000

47 Monitor.PulseAll (ja)

48 Monitor. Ukončiť (ja)

49 Slučka

50 End Sub

51 Verejná spotreba ()

52 MsgBox („Som vo vlákne konzumácie“)

53 Do

54 Monitor. Zadajte (ja)

55 Do While Me.OurMoney = 0

56 Monitor. Počkajte (ja)

57 Slučka

58 Console.WriteLine („Drahý rodič, práve som minul všetky tvoje“ & _

peniaze za týždeň “& TheWeek)

59 Týždeň + = 1

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

61 me. Naše peniaze = 0

62 Monitor.PulseAll (Me)

63 Monitor. Ukončiť (ja)

64 Slučka

65 End Sub

66 Koncová trieda

Metóda StartltsLife (riadky 13-22) sa pripravuje na spustenie streamov Produce and Consume. To najdôležitejšie sa deje v prúdoch Produce (riadky 39-50) a Consume (riadky 51-65). Procedúra Sub Produce kontroluje dostupnosť peňazí a ak peniaze existujú, idú do čakacej fronty. V opačnom prípade rodič vygeneruje peniaze (riadok 46) a upozorní objekty vo fronte na čakanie na zmenu situácie. Upozorňujeme, že volanie na Pulse-Pulse All sa prejaví iba vtedy, ak je zámka uvoľnená príkazom Monitor.Exit. Naopak, postup Sub Consume kontroluje dostupnosť peňazí, a ak peniaze nie sú, upozorní na to nastávajúceho rodiča. Riadok 60 jednoducho ukončí program po 21 podmienečných rokoch; systém volania. Environment.Exit (0) je .NET analóg príkazu End (príkaz End je tiež podporovaný, ale na rozdiel od System. Environment. Exit nevracia kód ukončenia operačnému systému).

Vlákna zaradené do čakacej fronty musia uvoľniť iné časti vášho programu. Z tohto dôvodu dávame prednosť použitiu PulseAll pred Pulse. Pretože nie je vopred známe, ktoré vlákno sa aktivuje pri volaní Pulse 1, ak je vo fronte relatívne málo vlákien, môžete rovnako dobre zavolať na PulseAll.

Viacvláknové spracovanie v grafických programoch

Naša diskusia o multithreadingu v aplikáciách GUI začína príkladom, ktorý vysvetľuje, na čo slúži multithreading v aplikáciách GUI. Vytvorte formulár pomocou dvoch tlačidiel Štart (btnStart) a Zrušiť (btnCancel), ako je znázornené na obr. 10.9. Kliknutím na tlačidlo Štart sa vygeneruje trieda, ktorá obsahuje náhodný reťazec s 10 miliónmi znakov, a spôsob počítania výskytov písmena „E“ v tomto dlhom reťazci. Všimnite si použitie triedy StringBuilder na efektívnejšie vytváranie dlhých reťazcov.

Krok 1

Vlákno 1 si všimne, že pre neho neexistujú žiadne údaje. Zavolá Počkajte, uvoľní zámok a prejde do poradia čakania.



Krok 2

Keď sa zámok uvoľní, vlákno 2 alebo vlákno 3 opustí rad blokov a vstúpi do synchronizovaného bloku, pričom získa zámok

Krok 3

Povedzme, že vlákno 3 zadá synchronizovaný blok, vytvorí údaje a zavolá Pulse-Pulse All.

Hneď po opustení bloku a uvoľnení zámku sa vlákno 1 presunie do frontu vykonávania. Ak vlákno 3 volá Pluse, do frontu na spustenie vstúpi iba jedenvlákno, keď sa volá Pluse All, všetky vlákna prejdú do frontu na spustenie.



Ryža. 10.8. Problém poskytovateľa / spotrebiteľa

Ryža. 10.9. Multithreading v jednoduchej aplikácii GUI

Importuje System.Text

Verejná trieda Náhodné znaky

Súkromné ​​m_Data ako StringBuilder

Súkromná dĺžka, m_počet ako celé číslo

Verejná čiastková novinka (ByVal n ako celé číslo)

m_dĺžka = n -1

m_Data = Nový StringBuilder (m_length) MakeString ()

Koniec pod

Private Sub MakeString ()

Dim i As Integer

Dim myRnd As New Random ()

Pre i = 0 až m_dĺžka

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

„previesť na veľké písmená

"a pripojte k objektu StringBuilder

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

Ďalšie

Koniec pod

Verejný čiastkový počiatočný počet ()

GetEes ()

Koniec pod

Private Sub GetEes ()

Dim i As Integer

Pre i = 0 až m_dĺžka

Ak m_Data.Chars (i) = CChar ("E") Potom

m_count + = 1

Ukončiť ako ďalšie

m_CountDone = Pravda

Koniec pod

Verejné iba na čítanie

Vlastníctvo GetCount () ako celé číslo

Ak nie (m_CountDone) Potom

Vrátiť m_count

Koniec Ak

Vlastníctvo End Get End

Verejné iba na čítanie

Property IsDone () ako Boolean Get

Návrat

m_CountDone

End Get

Koncová nehnuteľnosť

Koncová trieda

S dvoma tlačidlami vo formulári je spojený veľmi jednoduchý kód. Procedúra btn-Start_Click vytvorí inštanciu vyššie uvedenej triedy RandomCharacters, ktorá zapuzdruje reťazec s 10 miliónmi znakov:

Private Sub btnStart_Click (ByVal sender As System.Object.

ByVal e As System.EventArgs) Zvláda btnSTart.Click

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

RC.StartCount ()

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

Koniec pod

Tlačidlo Zrušiť zobrazí okno so správou:

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

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

MsgBox („Počet prerušených!“)

Koniec pod

Keď je program spustený a stlačí sa tlačidlo Štart, ukáže sa, že tlačidlo Zrušiť nereaguje na vstup používateľa, pretože súvislá slučka bráni tlačidlu spracovať prijatú udalosť. To je v moderných programoch neprijateľné!

Existujú dve možné riešenia. Prvá možnosť, dobre známa z predchádzajúcich verzií VB, upúšťa od viacvláknového volania: hovor DoEvents je súčasťou slučky. V NET tento príkaz vyzerá takto:

Application.DoEvents ()

V našom prípade to rozhodne nie je žiaduce - kto chce spomaliť program pomocou desiatich miliónov hovorov DoEvents! Ak namiesto toho priradíte slučku do samostatného vlákna, operačný systém bude prepínať medzi vláknami a tlačidlo Zrušiť zostane funkčné. Implementácia so samostatným vláknom je uvedená nižšie. Aby sme jasne ukázali, že tlačidlo Zrušiť funguje, keď naň klikneme, program jednoducho ukončíme.

Ďalší krok: tlačidlo Zobraziť počet

Povedzme, že ste sa rozhodli ukázať svoju tvorivú predstavivosť a dať formuláru vzhľad zobrazený na obr. 10.9. Poznámka: Tlačidlo Zobraziť počet ešte nie je k dispozícii.

Ryža. 10.10. Uzamknutý formulár tlačidla

Očakáva sa, že samostatné vlákno vykoná počítanie a odomkne nedostupné tlačidlo. To sa samozrejme dá urobiť; navyše takáto úloha vzniká pomerne často. Žiaľ, nebudete môcť konať najzrejmejším spôsobom - prepojte sekundárne vlákno s vláknom GUI tak, že v konštruktore ponecháte odkaz na tlačidlo ShowCount, alebo dokonca použijete štandardného delegáta. Inými slovami, nikdy nepoužívajte nižšie uvedenú možnosť (základná chybný riadky sú hrubé).

Verejná trieda Náhodné znaky

Súkromná m_0ata ako StringBuilder

Súkromné ​​m_CountDone ako booleovský

Súkromná dĺžka. m_count ako celé číslo

Súkromné ​​m_Button ako Windows.Forms.Button

Verejný čiastkový nový (ByVa1 n ako celé číslo, _

ByVal b ako Windows.Forms.Button)

m_dĺžka = n - 1

m_Data = Nový StringBuilder (mJength)

m_Button = b MakeString ()

Koniec pod

Private Sub MakeString ()

Dim I As Integer

Dim myRnd As New Random ()

Pre I = 0 až m_dĺžka

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

Ďalšie

Koniec pod

Verejný čiastkový počiatočný počet ()

GetEes ()

Koniec pod

Private Sub GetEes ()

Dim I As Integer

Pre I = 0 do dĺžky

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

m_count + = 1

Ukončiť ako ďalšie

m_CountDone = Pravda

m_Button.Enabled = Pravda

Koniec pod

Verejné iba na čítanie

Vlastníctvo GetCount () ako celé číslo

Dostať

Ak nie (m_CountDone) Potom

Hoďte novú výnimku („Počet ešte nebol dokončený“)

Vrátiť m_count

Koniec Ak

End Get

Koncová nehnuteľnosť

Verejný majetok len na čítanie je vykonaný () ako booleovský

Dostať

Vrátiť m_CountDone

End Get

Koncová nehnuteľnosť

Koncová trieda

Je pravdepodobné, že tento kód bude v niektorých prípadoch fungovať. Napriek tomu:

  • Interakciu sekundárneho vlákna s vláknom vytvárajúcim GUI nie je možné organizovať očividné prostriedky.
  • Nikdy neupravujte prvky v grafických programoch z iných prúdov programov. Všetky zmeny by sa mali vyskytnúť iba vo vlákne, ktoré vytvorilo GUI.

Ak porušíte tieto pravidlá, my garantujeme tieto jemné a jemné chyby sa vyskytnú vo vašich viacvláknových grafických programoch.

Tiež nebude možné organizovať interakciu predmetov pomocou udalostí. Pracovník pre 06 udalostí beží na rovnakom vlákne, ako bol nazývaný RaiseEvent, takže vám udalosti nepomôžu.

Napriek tomu zdravý rozum hovorí, že grafické aplikácie musia mať prostriedky na úpravu prvkov z iného vlákna. V rozhraní NET Framework existuje spôsob, ako bezpečne volať metódy GUI aplikácií z iného vlákna. Na tento účel sa používa špeciálny typ delegáta Method Invoker z priestoru názvov System.Windows. Formuláre. Nasledujúci úryvok zobrazuje novú verziu metódy GetEes (riadky sú zmenené tučným písmom):

Private Sub GetEes ()

Dim I As Integer

Pre I = 0 až m_dĺžka

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

m_count + = 1

Ukončiť ako ďalšie

m_CountDone = Skutočný pokus

Dim mylnvoker ako nová metódalnvoker (AddressOf UpDateButton)

myInvoker.Invoke () Catch e As ThreadlnterruptedException

„Zlyhanie

Koniec pokusu

Koniec pod

Verejné tlačidlo Sub UpDateButton ()

m_Button.Enabled = Pravda

Koniec pod

Inter-vláknové hovory na tlačidlo sa neuskutočňujú priamo, ale prostredníctvom Method Invoker. Rozhranie .NET Framework zaručuje, že táto možnosť je bezpečná pre vlákna.

Prečo je pri viacvláknovom programovaní toľko problémov?

Teraz, keď už trochu rozumiete multithreadingu a potenciálnym problémom s ním spojeným, rozhodli sme sa, že by bolo vhodné zodpovedať si otázku v nadpise tohto pododdielu na konci tejto kapitoly.

Jeden z dôvodov je ten, že viacvláknové spracovanie je nelineárny proces a sme zvyknutí na lineárny programovací model. Spočiatku je ťažké zvyknúť si na myšlienku, že spustenie programu je možné náhodne prerušiť a riadenie sa prenesie do iného kódu.

Existuje však ešte jeden, zásadnejší dôvod: v dnešnej dobe programátori príliš zriedka programujú v assembleri alebo sa aspoň pozerajú na rozobratý výstup kompilátora. V opačnom prípade by si oveľa jednoduchšie zvykli na myšlienku, že desiatky montážnych návodov môžu zodpovedať jednému príkazu jazyka na vysokej úrovni (napríklad VB .NET). Vlákno môže byť prerušené po ktoromkoľvek z týchto pokynov, a preto uprostred príkazu na vysokej úrovni.

Ale to nie je všetko: moderné kompilátory optimalizujú výkon programu a hardvér počítača môže interferovať so správou pamäte. Výsledkom je, že kompilátor alebo hardvér môže bez vášho vedomia zmeniť poradie príkazov uvedených v zdrojovom kóde programu [ Mnoho kompilátorov optimalizuje cyklické kopírovanie polí ako pre i = 0 až n: b (i) = a (i): ncxt. Kompilátor (alebo dokonca špecializovaný správca pamäte) môže jednoducho vytvoriť pole a potom ho mnohokrát kopírovať jednotlivé prvky vyplniť jednou operáciou kopírovania!].

Našťastie vám tieto vysvetlenia pomôžu lepšie porozumieť tomu, prečo viacvláknové programovanie spôsobuje toľko problémov - alebo prinajmenšom menšie prekvapenie nad podivným správaním vašich viacvláknových programov!

Príklad budovania jednoduchej viacvláknovej aplikácie.

Narodený kvôli mnohým otázkam týkajúcim sa vytvárania viacvláknových aplikácií v Delphi.

Účelom tohto príkladu je demonštrovať, ako správne zostaviť viacvláknovú aplikáciu, pričom dlhodobú prácu vezmeme do samostatného vlákna. A ako v takejto aplikácii zabezpečiť interakciu hlavného vlákna s pracovníkom na prenos údajov z formulára (vizuálne komponenty) do prúdu a naopak.

Príklad netvrdí, že je úplný, iba ukazuje najjednoduchšie spôsoby interakcie medzi vláknami. Umožniť používateľovi „rýchlo oslepiť“ (kto by vedel, ako to nenávidím) správne fungujúcu viacvláknovú aplikáciu.
V ňom je všetko podrobne komentované (podľa mňa), ale ak máte nejaké otázky, opýtajte sa.
Ale ešte raz vás varujem: Streamy nie sú jednoduché... Ak nemáte predstavu, ako to všetko funguje, potom existuje obrovské nebezpečenstvo, že často vám všetko bude fungovať dobre a niekedy sa program bude správať viac ako divne. Správanie nesprávne napísaného viacvláknového programu veľmi závisí od veľkého počtu faktorov, ktoré sa niekedy počas ladenia nedajú reprodukovať.

Takže príklad. Pre jednoduchosť som vložil kód a priložil archív k modulu a kódu formulára

jednotka ExThreadForm;

používa
Windows, Správy, SysUtils, Varianty, Triedy, Grafika, Ovládacie prvky, Formuláre,
Dialógy, StdCtrls;

// konštanty používané pri prenose údajov z prúdu do formulára pomocou
// odosielanie správ z okna
konšt
WM_USER_SendMessageMetod = WM_USER + 10;
WM_USER_PostMessageMetod = WM_USER + 11;

typ
// popis triedy vlákna, potomok tThread
tMyThread = trieda (tThread)
súkromné
SyncDataN: celé číslo;
SyncDataS: Reťazec;
procedúra SyncMetod1;
chránené
postup Vykonať; potlačiť;
verejná
Param1: Reťazec;
Param2: Integer;
Param3: Boolean;
Zastavené: booleovský;
LastRandom: Integer;
IterationNo: Integer;
ResultList: tStringList;

Vytvorenie konštruktora (aParam1: reťazec);
ničiteľ zničiť; potlačiť;
koniec;

// popis triedy formulára pomocou streamu
TForm1 = trieda (TForm)
Label1: TLabel;
Memo1: TMemo;
btnStart: TButton;
btnStop: TButton;
Edit1: TEdit;
Edit2: TEdit;
CheckBox1: TCheckBox;
Label2: TLabel;
Label3: TLabel;
Label4: TLabel;
postup btnStartClick (Odosielateľ: TObject);
postup btnStopClick (Odosielateľ: TObject);
súkromné
(Súkromné ​​vyhlásenia)
Môj vlákno: tMyThread;
procedúra EventMyThreadOnTerminate (Odosielateľ: tObject);
postup EventOnSendMessageMetod (var Msg: TMessage); správa WM_USER_SendMessageMetod;
procedúra EventOnPostMessageMetod (var Msg: TMessage); správa WM_USER_PostMessageMetod;

Verejné
(Verejné vyhlásenia)
koniec;

var
Forma1: TForm1;

{
Zastavené - Ukazuje prenos údajov z formulára do prúdu.
Dodatočná synchronizácia nie je potrebná, pretože je jednoduchá
je jednoslovný a je napísaný iba jedným vláknom.
}

procedúra TForm1.btnStartClick (Odosielateľ: TObject);
začať
Randomize (); // zaistenie náhodnosti v sekvencii pomocou Random () - nemá nič spoločné s tokom

// Vytvorte inštanciu objektu stream a odovzdajte mu vstupný parameter
{
POZOR!
Konštruktor streamu je zapísaný tak, že je vytvorený prúd
pozastavené, ako to umožňuje:
1. Ovládajte okamih jeho spustenia. To je takmer vždy pohodlnejšie, pretože
umožňuje nastaviť stream ešte pred spustením, odovzdať ho vstupu
parametre atd.
2. Pretože odkaz na vytvorený objekt sa potom uloží do poľa formulára
po samodeštrukcii vlákna (pozri nižšie), ktoré keď je vlákno spustené
sa môže vyskytnúť kedykoľvek, tento odkaz bude neplatný.
}
MyThread: = tMyThread.Create (Form1.Edit1.Text);

// Keďže však bolo vlákno vytvorené pozastavené, potom o akýchkoľvek chybách
// pri jeho inicializácii (pred štartom) si ho musíme zničiť sami
// na čo používame try / okrem block
skúsiť

// Priradenie obsluhy ukončenia vlákna, do ktorej dostaneme
// výsledky práce streamu, a „prepísať“ naň odkaz
MyThread.OnTerminate: = EventMyThreadOnTerminate;

// Pretože výsledky budú zhromaždené v OnTerminate, t.j. pred sebazničením
// potok potom odstránime starosti s jeho zničením
MyThread.FreeOnTerminate: = True;

// Príklad prechodu vstupných parametrov cez polia objektu stream v bode
// inštancia, pokiaľ ešte nie je spustená.
// Osobne to radšej robím prostredníctvom parametrov prepísaného
// konštruktor (tMyThread.Create)
MyThread.Param2: = StrToInt (Form1.Edit2.Text);

MyThread.Stopped: = False; // aj druh parametra, ale meniaci sa v
// doba chodu vlákna
okrem
// keďže vlákno ešte nebolo spustené a nebude sa dať sebazničiť, zničíme ho "ručne"
FreeAndNil (MyThread);
// a potom nech je výnimka spracovaná ako obvykle
zvýšiť;
koniec;

// Pretože bol objekt vlákna úspešne vytvorený a nakonfigurovaný, je načase ho spustiť
MyThread.Resume;

ShowMessage („Stream spustený“);
koniec;

procedúra TForm1.btnStopClick (Odosielateľ: TObject);
začať
// Ak inštancia vlákna stále existuje, požiadajte ho o zastavenie
// A presne "opýtajte sa". V zásade môžeme aj „nútiť“, ale bude
// mimoriadne núdzová možnosť, ktorá vyžaduje jasné pochopenie tohto všetkého
// streamovaná kuchyňa. Preto sa tu neuvažuje.
ak je priradené (MyThread) potom
MyThread.Stopped: = Pravda
inak
ShowMessage („Vlákno nie je spustené!“);
koniec;

procedúra TForm1.EventOnSendMessageMetod (var Msg: TMessage);
začať
// spôsob spracovania synchrónnej správy
// vo WParam adresa objektu tMyThread, v LParam aktuálna hodnota LastRandom vlákna
s tMyThread (Msg.WParam) začnú
Form1.Label3.Popis: = Formát ("% d% d% d",);
koniec;
koniec;

procedúra TForm1.EventOnPostMessageMetod (var Msg: TMessage);
začať
// metóda na spracovanie asynchrónnej správy
// vo WParam aktuálna hodnota IterationNo, v LParam aktuálna hodnota streamu LastRandom
Form1.Label4.Caption: = Formát ("% d% d",);
koniec;

procedúra TForm1.EventMyThreadOnTerminate (Odosielateľ: tObject);
začať
// DÔLEŽITÉ!
// Metóda spracovania udalosti OnTerminate sa vždy volá v kontexte main
// vlákno - to zaručuje implementácia tThread. Preto v ňom môžete voľne
// použite akékoľvek vlastnosti a metódy akýchkoľvek objektov

// Pre každý prípad sa uistite, že inštancia objektu stále existuje
ak nie je priradené (MyThread), potom Ukončiť; // ak tam nie je, tak sa neda nic robit

// získajte výsledky práce závitu inštancie objektu vlákna
Form1.Memo1.Lines.Add (Format ("Stream sa skončil výsledkom% d",));
Form1.Memo1.Lines.AddStrings ((Odosielateľ ako tMyThread) .ResultList);

// Zničte odkaz na inštanciu objektu stream.
// Pretože sa naše vlákno samo-ničí (FreeOnTerminate: = True)
// potom po dokončení obsluhy OnTerminate bude inštancia objektu stream
// je zničený (bezplatný) a všetky odkazy naň budú neplatné.
// Aby ste na takýto odkaz náhodou nenarazili, prepíšte MyThread
// Ešte raz poznamenám - nezničíme objekt, ale iba prepíšeme odkaz. Objekt
// zničí sa!
Môj vlákno: = Nula;
koniec;

konštruktor tMyThread.Create (aParam1: String);
začať
// Vytvorte inštanciu SUSPENDED streamu (pozrite si komentár pri vytváraní inštancie)
zdedené Create (True);

// Vytvorte interné objekty (ak je to potrebné)
ResultList: = tStringList.Create;

// Získať počiatočné údaje.

// Skopírujte vstupné údaje prechádzajúce parametrom
Param1: = aParam1;

// Príklad prijatia vstupných údajov z komponentov VCL v konštruktore objektu streamu
// To je v tomto prípade prijateľné, pretože konštruktor sa nazýva v kontexte
// hlavné vlákno. Preto je tu prístup k komponentom VCL.
// Ale toto sa mi nepáči, pretože si myslím, že je zlé, keď vlákno niečo vie
// o nejakej forme tam. Ale čo nemôžete urobiť pre ukážku.
Param3: = Form1.CheckBox1.Checked;
koniec;

destruktor tMyThread.Destroy;
začať
// zničenie vnútorných predmetov
FreeAndNil (ResultList);
// zničiť základný tThread
zdedené;
koniec;

procedúra tMyThread.Execute;
var
t: kardinál;
s: Reťazec;
začať
IterationNo: = 0; // počítadlo výsledkov (číslo cyklu)

// V mojom prípade je telom vlákna slučka, ktorá končí
// alebo externou „požiadavkou“ na ukončenie prechádzajúcou cez parameter premennej Zastavené,
// buď len urobením 5 slučiek
// Je pre mňa príjemnejšie písať to prostredníctvom „večnej“ slučky.

Kým True začať

Inc (IterationNo); // cislo dalsieho cyklu

LastRandom: = Náhodné (1000); // číslo kľúča - na ukážku prenosu parametrov zo streamu do formulára

T: = Náhodné (5) +1; // čas, na ktorý zaspíme, ak nie sme dokončení

// Hlúpa práca (v závislosti od vstupného parametra)
ak nie Param3, potom
Inc (Param2)
inak
Dec (Param2);

// Vytvorte priebežný výsledok
s: = Formát ("% s% 5d% s% d% d",
);

// Pridanie priebežného výsledku do zoznamu výsledkov
ResultList.Add (s);

//// Príklady odovzdania medziproduktu do formulára

//// Prechod synchronizovanou metódou - klasickým spôsobom
//// Nevýhody:
//// - synchronizovaná metóda je zvyčajne metóda triedy stream (na prístup
//// do polí objektu stream), ale na prístup k poliam formulára musí
//// "vedieť" o ňom a o jeho poliach (objektoch), s čím sa zvyčajne veľmi nedarí
//// uhol pohľadu na organizáciu programu.
//// - aktuálne vlákno bude pozastavené, kým sa nedokončí spustenie
//// synchronizovaná metóda.

//// Výhody:
//// - štandardné a všestranné
//// - v synchronizovanej metóde môžete použiť
//// všetky polia objektu streamu.
// najskôr, ak je to potrebné, musíte preniesť prenesené údaje do
// špeciálne polia objektu objektu.
SyncDataN: = IterationNo;
SyncDataS: = "Synchronizácia" + s;
// a potom poskytnite synchronizované volanie metódy
Synchronizovať (SyncMetod1);

//// Odosielanie prostredníctvom synchrónneho odosielania správ (SendMessage)
//// v tomto prípade môžu byť údaje prenášané prostredníctvom parametrov správy (LastRandom),
//// a cez polia objektu, pričom v parametri správy odovzdáte adresu inštancie
//// objektu streamu - Integer (Self).
//// Nevýhody:
//// - vlákno musí poznať popisovač okna formulára
//// - ako pri synchronizácii, aktuálne vlákno bude pozastavené do
//// dokončenie spracovania správy hlavným vláknom
//// - vyžaduje značné množstvo času CPU pre každý hovor
//// (na prepínanie vlákien), preto je veľmi časté volanie nežiaduce
//// Výhody:
//// - ako pri synchronizácii, aj pri spracovaní správy môžete použiť
//// všetky polia objektu streamu (ak bola samozrejme zadaná jeho adresa)


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

//// Prenos prostredníctvom asynchrónneho odosielania správ (PostMessage)
//// Pretože v tomto prípade v čase prijatia správy hlavným vláknom,
//// stream odosielania môže byť už dokončený, pričom bola odoslaná adresa inštancie
//// objekt streamu je neplatný!
//// Nevýhody:
//// - vlákno musí poznať popisovač okna formulára;
//// - z dôvodu asynchrónie je prenos údajov možný iba prostredníctvom parametrov
//// správy, čo výrazne komplikuje prenos dát, ktoré majú veľkosť
//// viac ako dve strojové slová. Je vhodné použiť na odovzdávanie celých čísel atď.
//// Výhody:
//// - na rozdiel od predchádzajúcich metód aktuálne vlákno NEBUDE
//// je pozastavené a okamžite obnoví spustenie
//// - na rozdiel od synchronizovaného hovoru, obsluha správ
//// je metóda formulára, ktorá musí mať znalosti o objekte streamu,
//// alebo o streame neviete vôbec nič, ak sa prenášajú iba údaje
//// prostredníctvom parametrov správy. To znamená, že vlákno nemusí vedieť nič o tvare.
//// všeobecne - iba jej Handle, ktorú je možné predtým odovzdať ako parameter
//// založ vlákno.
PostMessage (Form1.Handle, WM_USER_PostMessageMetod, IterationNo, LastRandom);

//// Skontrolujte možné dokončenie

// Kontrola dokončenia podľa parametra
ak je zastavený, potom Break;

// Príležitostne skontrolujte dokončenie
ak IterationNo> = 10, potom Break;

Spánok (t * 1 000); // Zaspi na t sekund
koniec;
koniec;

procedúra tMyThread.SyncMetod1;
začať
// táto metóda sa volá pomocou metódy Synchronize.
// To je napriek tomu, že ide o metódu vlákna tMyThread,
// beží v kontexte hlavného vlákna aplikácie.
// Preto môže robiť čokoľvek, dobre alebo takmer všetko :)
// Ale pamätajte si, že nemá cenu sa tu dlho "motať"

// Odovzdané parametre môžeme extrahovať zo špeciálnych polí, kde ich máme
// uložené pred volaním.
Form1.Label1.Caption: = SyncDataS;

// buď z iných polí objektu streamu, napríklad podľa jeho aktuálneho stavu
Form1.Label2.Caption: = Formát ("% d% d",);
koniec;

Vo všeobecnosti príkladu predchádzalo moje nasledujúce zdôvodnenie k téme ...

Po prvé:
Najdôležitejšie pravidlo viacvláknového programovania v Delphi je:
V kontexte iného ako hlavného vlákna nie je možné získať prístup k vlastnostiam a metódam formulárov a skutočne ku všetkým komponentom, ktoré „rastú“ z tWinControl.

To znamená (trochu zjednodušené), že ani v metóde Execute zdedenej z TThread, ani v iných metódach / postupoch / funkciách volaných z Execute, je zakázané priamy prístup k akýmkoľvek vlastnostiam a metódam vizuálnych komponentov.

Ako to urobiť správne.
Neexistujú jednotné recepty. Presnejšie povedané, existuje toľko a rôznych možností, že si v závislosti od konkrétneho prípadu musíte vybrať. Preto odkazujú na článok. Po prečítaní a porozumení bude programátor schopný porozumieť a ako to v konkrétnom prípade najlepšie urobiť.

V skratke na prstoch:

Viacvláknová aplikácia sa najčastejšie stáva buď vtedy, keď je potrebné vykonať nejaký druh dlhodobej práce, alebo keď je možné súčasne vykonávať niekoľko vecí, ktoré výrazne nezaťažujú procesor.

V prvom prípade implementácia práce vo vnútri hlavného vlákna vedie k „spomaleniu“ používateľského rozhrania - počas práce sa slučka správ nevykonáva. Výsledkom je, že program nereaguje na akcie používateľov a formulár sa nevykreslí, napríklad keď ho používateľ presunie.

V druhom prípade, keď práca zahŕňa aktívnu výmenu s vonkajším svetom, potom počas nútených „prestojov“. Počas čakania na príjem / odosielanie údajov môžete súbežne robiť niečo iné, napríklad znova odosielať / prijímať údaje.

Existujú aj iné prípady, ale menej často. To však nie je dôležité. Teraz to nie je o tom.

Teraz, ako je to všetko napísané. Prirodzene sa zvažuje určitý najčastejší prípad, trochu zovšeobecnený. Takže.

Práca vykonaná v samostatnom vlákne má vo všeobecnosti štyri entity (neviem, ako to nazvať presnejšie):
1. Počiatočné údaje
2. V skutočnosti samotná práca (môže to závisieť od pôvodných údajov)
3. Medziľahlé údaje (napríklad informácie o aktuálnom stave vykonávania práce)
4. Výstupné údaje (výsledok)

Na čítanie a zobrazenie väčšiny údajov sa najčastejšie používajú vizuálne komponenty. Ako je však uvedené vyššie, zo streamu nemáte priamy prístup k vizuálnym komponentom. Ako byť?
Vývojári Delphi navrhujú použiť metódu Synchronize triedy TThread. Tu nebudem popisovať, ako ho používať - ​​existuje na to spomínaný článok. Dovoľte mi povedať, že jeho aplikácia, dokonca ani tá správna, nie je vždy odôvodnená. Existujú dva problémy:

Po prvé, telo metódy nazývanej prostredníctvom Synchronize sa vždy vykoná v kontexte hlavného vlákna, a preto sa počas vykonávania slučka okenných správ opäť nespustí. Preto sa musí vykonať rýchlo, inak budeme mať rovnaké problémy ako pri implementácii s jedným vláknom. V ideálnom prípade by sa metóda nazývaná prostredníctvom Synchronizovať mala vo všeobecnosti používať iba na prístup k vlastnostiam a metódam vizuálnych objektov.

Za druhé, spustenie metódy prostredníctvom synchronizácie je „drahé“ potešenie z dôvodu potreby dvoch prepínačov medzi vláknami.

Oba problémy sú navyše prepojené a spôsobujú rozpor: na jednej strane na vyriešenie prvého je potrebné „rozomlieť“ metódy volané prostredníctvom synchronizácie a na druhej strane ich potom treba volať častejšie, pričom stratíte vzácnosť prostriedky procesora.

Preto, ako vždy, je potrebné pristupovať rozumne a v rôznych prípadoch používať rôzne spôsoby interakcie toku s vonkajším svetom:

Počiatočné údaje
Všetky údaje, ktoré sú prenášané do streamu a ktoré sa počas jeho prevádzky nemenia, musia byť prenesené ešte pred ich spustením, t.j. pri vytváraní streamu. Aby ste ich mohli použiť v tele vlákna, musíte si ich vytvoriť lokálne (zvyčajne v poliach potomka TThread).
Ak existujú počiatočné údaje, ktoré sa môžu počas spustenia vlákna zmeniť, potom k nim treba pristupovať buď synchronizovanými metódami (metódy volané prostredníctvom synchronizácie), alebo prostredníctvom polí objektu vlákna (potomok TThread). Ten druhý vyžaduje určitú opatrnosť.

Stredné a výstupné údaje
Tu opäť existuje niekoľko spôsobov (v poradí podľa mojich preferencií):
- Spôsob asynchrónneho odosielania správ do hlavného okna aplikácie.
Obvykle sa používa na odosielanie správ o priebehu procesu do hlavného okna aplikácie s prenosom malého množstva údajov (napríklad percento dokončenia)
- Spôsob synchrónneho odosielania správ do hlavného okna aplikácie.
Obvykle sa používa na rovnaké účely ako asynchrónne odosielanie, ale umožňuje vám prenášať väčšie množstvo údajov bez vytvárania samostatnej kópie.
- Synchronizované metódy, ak je to možné, kombinujúce prenos čo najväčšieho počtu údajov do jednej metódy.
Možno použiť aj na načítanie údajov z formulára.
- Prostredníctvom polí objektu toku poskytuje vzájomne sa vylučujúci prístup.
Viac podrobností nájdete v článku.

Eh. Krátko to nevyšlo