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:
- 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.
- 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.
- 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. - 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.
- 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.
- 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