За какви цели се използват многонишкови системи. Осем прости правила за разработване на многонишкови приложения

Коя тема повдига най -много въпроси и трудности за начинаещите? Когато попитах моя учител и Java програмист Александър Пряхин за това, той веднага ми отговори: „Многопоточност“. Благодаря му за идеята и помощта при подготовката на тази статия!

Ще разгледаме вътрешния свят на приложението и неговите процеси, ще разберем каква е същността на многопоточността, кога е полезна и как да я приложим - използвайки Java като пример. Ако изучавате различен език на ООП, не се притеснявайте: основните принципи са едни и същи.

За потоците и техния произход

За да разберем многопоточността, нека първо разберем какво е процес. Процесът е част от виртуална памет и ресурси, които ОС отделя за изпълнение на програма. Ако отворите няколко екземпляра на едно и също приложение, системата ще разпредели процес за всеки. В съвременните браузъри отделен процес може да бъде отговорен за всеки раздел.

Вероятно сте попаднали на Windows „Task Manager“ (в Linux това е „System Monitor“) и знаете, че ненужните работещи процеси зареждат системата, а най -„тежките“ от тях често замръзват, така че трябва да бъдат прекратени принудително .

Но потребителите обичат многозадачността: не ги хранете с хляб - оставете ги да отворят дузина прозорци и да скачат напред -назад. Има дилема: трябва да осигурите едновременната работа на приложенията и в същото време да намалите натоварването на системата, така че да не се забави. Да приемем, че хардуерът не може да се справи с нуждите на собствениците - трябва да решите проблема на ниво софтуер.

Искаме процесорът да изпълнява повече инструкции и да обработва повече данни за единица време. Тоест, трябва да поставим по -изпълнен код във всеки времеви отрязък. Мислете за единица за изпълнение на код като обект - това е нишка.

До сложен случай е по -лесно да се подходи, ако го разделите на няколко прости. Така е и при работа с памет: „тежък“ процес е разделен на нишки, които заемат по -малко ресурси и е по -вероятно да доставят кода на калкулатора (как точно - вижте по -долу).

Всяко приложение има поне един процес и всеки процес има поне една нишка, която се нарича основна нишка и от която се стартират нови, ако е необходимо.

Разлика между нишки и процеси

    Потоците използват паметта, разпределена за процеса, а процесите изискват собствено пространство в паметта. Следователно нишките се създават и завършват по -бързо: системата не трябва да им разпределя ново адресно пространство всеки път и след това да го освобождава.

    Всеки процес работи със собствени данни - те могат да обменят нещо само чрез механизма на междупроцесна комуникация. Темите имат директен достъп до данните и ресурсите на другите: това, което е променено, е незабавно достъпно за всички. Потокът може да контролира „колегата“ в процеса, докато процесът контролира изключително своите „дъщери“. Следователно превключването между потоци е по -бързо и комуникацията между тях е по -лесна.

Какъв е изводът от това? Ако трябва да обработите голямо количество данни възможно най -бързо, разделете ги на парчета, които могат да бъдат обработени от отделни нишки, и след това съберете резултата заедно. Това е по-добре от създаването на процеси, гладни на ресурси.

Но защо популярно приложение като Firefox върви по пътя на създаване на множество процеси? Тъй като за браузъра работата на изолирани раздели е надеждна и гъвкава. Ако нещо не е наред с един процес, не е необходимо да прекратявате цялата програма - възможно е да запазите поне част от данните.

Какво е многонишково

Така стигаме до основния момент. Многопоточността е, когато процесът на кандидатстване се разделя на нишки, които се обработват паралелно - за една единица време - от процесора.

Изчислителното натоварване се разпределя между две или повече ядра, така че интерфейсът и другите компоненти на програмата да не забавят работата си.

Многопоточните приложения могат да се изпълняват и на едноядрени процесори, но след това нишките се изпълняват на свой ред: първият работи, състоянието му е запазено - второто е разрешено да работи, запазено - връща се към първото или стартира третото, и т.н.

Заетите хора се оплакват, че имат само две ръце. Процесите и програмите могат да имат колкото се може повече ръце, за да изпълнят задачата възможно най -бързо.

Изчакайте сигнал: синхронизация в многопоточни приложения

Представете си, че няколко нишки се опитват да променят една и съща област от данни едновременно. Чии промени в крайна сметка ще бъдат приети и чии промени ще бъдат отменени? За да се избегне объркване при работа със споделени ресурси, нишките трябва да координират действията си. За да направят това, те обменят информация с помощта на сигнали. Всяка нишка казва на другите какво прави и какви промени да очаквате. Така данните на всички нишки за текущото състояние на ресурсите се синхронизират.

Основни инструменти за синхронизация

Взаимно изключване (взаимно изключване, съкратено - mutex) - "флаг", отиващ в нишката, на който понастоящем е разрешено да работи със споделени ресурси. Премахва достъпа от други нишки до заеманата област на паметта. В приложението може да има няколко мутекса и те могат да се споделят между процесите. Има един улов: mutex принуждава приложението да получава достъп до ядрото на операционната система всеки път, което е скъпо.

Семафор - позволява ви да ограничите броя на нишките, които имат достъп до ресурс в даден момент. Това ще намали натоварването на процесора при изпълнение на код, където има тесни места. Проблемът е, че оптималният брой нишки зависи от машината на потребителя.

Събитие - дефинирате условие, при възникването на което управлението се прехвърля към желаната нишка. Потоците обменят данни за събития, за да развиват и логически да продължават действията си. Единият е получил данните, другият е проверил коректността му, третият ги е записал на твърдия диск. Събитията се различават по начина, по който се анулират. Ако трябва да уведомите няколко теми за събитие, ще трябва ръчно да настроите функцията за отмяна, за да спрете сигнала. Ако има само една целева нишка, можете да създадете събитие за автоматично нулиране. Той ще спре самия сигнал, след като достигне потока. Събитията могат да бъдат поставени на опашка за гъвкав контрол на потока.

Критичен раздел - по -сложен механизъм, който комбинира брояч на цикъл и семафор. Броячът ви позволява да отложите стартирането на семафора за желаното време. Предимството е, че ядрото се активира само ако секцията е заета и семафорът трябва да бъде включен. През останалото време нишката работи в потребителски режим. Уви, раздел може да се използва само в рамките на един процес.

Как да приложим многопоточност в Java

Класът Thread е отговорен за работата с нишки в Java. Създаването на нова нишка за изпълнение на задача означава създаване на екземпляр от класа Thread и свързването му с кода, който искате. Това може да стане по два начина:

    подклас Нишка;

    внедрете интерфейса Runnable във вашия клас и след това предайте екземплярите на класа на конструктора на нишката.

Въпреки че няма да засягаме темата за задънена улица, когато нишките си блокират работата и замръзват, ще оставим това за следващата статия.

Пример за многонишково изпълнение на Java: пинг-понг с мутекси

Ако мислите, че ще се случи нещо ужасно, издишайте. Ще обмислим работата с обекти за синхронизация почти по игрив начин: две нишки ще бъдат хвърлени от мутекс. Но всъщност ще видите истинско приложение, където само една нишка може да обработва публично достъпни данни наведнъж.

Първо, нека създадем клас, който наследява свойствата на нишката, която вече познаваме, и да напишем метод kickBall:

Публичен клас PingPongThread разширява Thread (PingPongThread (String name) (this.setName (name); // отменя името на нишката) @Override public void run () (Ball ball = Ball.getBall (); while (ball.isInGame () ) (kickBall (ball);)) private void kickBall (Ball ball) (if (! ball.getSide (). equals (getName ())) (ball.kick (getName ());)))

Сега нека се погрижим за топката. Той няма да бъде прост с нас, а запомнящ се: за да може да каже кой го е ударил, от коя страна и колко пъти. За да направим това, използваме мутекс: той ще събира информация за работата на всяка от нишките - това ще позволи на изолирани нишки да комуникират помежду си. След 15 -то попадение ще извадим топката от играта, за да не я нараним сериозно.

Публичен клас Ball (private int kicks = 0; private static Ball instance = new Ball (); private String side = ""; private Ball () () static Ball getBall () (връщане на екземпляр;) синхронизиран анулен удар (String playername) (kicks ++; side = playername; System.out.println (kicks + "" + side);) String getSide () (страна за връщане;) булева стойност isInGame () (return (kicks< 15); } }

И сега две нишки на плейъра влизат на сцената. Нека ги наречем, без да се замисляме, Ping и Pong:

Публичен клас PingPongGame (PingPongThread player1 = нов PingPongThread ("Ping"); PingPongThread player2 = нов PingPongThread ("Pong"); Топка с топка; PingPongGame () (ball = Ball.getBall ();) void startGame1 () изхвърля Interview .start (); player2.start ();))

„Пълен стадион от хора - време е да започнем мача.“ Официално ще обявим откриването на срещата - в основния клас на приложението:

Публичен клас PingPong (публичен static void main (String args) хвърля InterruptException (PingPongGame game = new PingPongGame (); game.startGame ();))

Както виждате, тук няма нищо яростно. Това засега е само въведение в многопоточността, но вече знаете как работи и можете да експериментирате - ограничете продължителността на играта не по броя на ударите, а по времето например. По -късно ще се върнем към темата за многопоточността - ще разгледаме пакета java.util.concurrent, библиотеката Akka и летливия механизъм. Нека поговорим и за внедряването на многопоточност в Python.

Многопоточното програмиране не се различава коренно от писането на графични потребителски интерфейси, управлявани от събития, или дори от писането на прости последователни приложения. Тук се прилагат всички важни правила, регулиращи капсулирането, разделянето на грижите, хлабавото свързване и т.н. Но много разработчици се затрудняват да пишат многонишкови програми именно защото пренебрегват тези правила. Вместо това те се опитват да приложат на практика много по -малко важните познания за нишките и примитивите за синхронизация, получени от текстовете за многонишково програмиране за начинаещи.

И така, какви са тези правила

Друг програмист, изправен пред проблем, мисли: "О, точно, трябва да приложим регулярни изрази." И сега той вече има два проблема - Джейми Завински.

Друг програмист, изправен пред проблем, мисли: „О, да, ще използвам потоци тук“. И сега той има десет проблема - Бил Шиндлер.

Твърде много програмисти, които се ангажират да напишат многопоточен код, попадат в капана, като героят от баладата на Гьоте " Чиракът на магьосника". Програмистът ще се научи как да създаде куп нишки, които по принцип работят, но рано или късно те излизат извън контрол и програмистът не знае какво да прави.

Но за разлика от отпадащия магьосник, нещастният програмист не може да се надява на пристигането на мощен магьосник, който ще махне с пръчката си и ще възстанови реда. Вместо това програмистът прави най -грозните трикове, опитвайки се да се справи с постоянно възникващите проблеми. Резултатът винаги е един и същ: получава се прекалено сложно, ограничено, крехко и ненадеждно приложение. Той има постоянна заплаха от задънена улица и други опасности, присъщи на лошия многонишков код. Дори не говоря за необясними сривове, лошо представяне, непълни или неправилни резултати от работата.

Може би сте се чудили: защо се случва това? Често срещано погрешно схващане е: "Многонишковото програмиране е много трудно." Но това не е така. Ако многопоточна програма е ненадеждна, тя обикновено се проваля по същите причини като нискокачествена еднопоточна програма. Просто програмистът не следва основните, добре познати и доказани методи за развитие. Многонишковите програми изглеждат само по -сложни, защото колкото повече паралелни нишки се объркат, толкова повече бъркотия правят - и много по -бързо, отколкото би направила една нишка.

Погрешното схващане за „сложността на многопоточното програмиране“ стана широко разпространено поради онези разработчици, които са се развили професионално в писането на еднопоточен код, за първи път са се сблъскали с многонишковост и не са се справили с него. Но вместо да преосмислят своите пристрастия и навици на работа, те упорито фиксират факта, че не искат да работят по никакъв начин. Извинявайки се за ненадежден софтуер и пропуснати срокове, тези хора повтарят едно и също: „многонишковото програмиране е много трудно“.

Моля, обърнете внимание, че по -горе говоря за типични програми, които използват многопоточност. Всъщност има сложни многопоточни сценарии-както и сложни еднопоточни. Но те не са често срещани. По правило от програмиста на практика не се изисква нищо свръхестествено. Преместваме данните, трансформираме ги, правим някои изчисления от време на време и накрая запазваме информацията в база данни или я показваме на екрана.

Няма нищо трудно в подобряването на средната еднопоточна програма и превръщането й в многопоточна. Поне не трябва да бъде. Трудностите възникват по две причини:

  • програмистите не знаят как да прилагат прости, добре известни доказани методи за разработка;
  • по-голямата част от информацията, представена в книги за многопоточно програмиране, е технически правилна, но напълно неприложима за решаване на приложни проблеми.

Най -важните концепции за програмиране са универсални. Те са еднакво приложими за еднопоточни и многопоточни програми. Програмистите, удавени в водовъртеж от потоци, просто не са научили важни уроци, когато са усвоили еднонишков код. Мога да кажа това, защото такива разработчици допускат същите фундаментални грешки в многопоточни и еднопоточни програми.

Може би най -важният урок, който трябва да се научи за шестдесет години история на програмирането, е: глобално променящо се състояние- зло... Истинско зло. Програмите, които разчитат на глобално променящо се състояние, са относително трудни за разсъждение и като цяло са ненадеждни, защото има твърде много начини за промяна на състоянието. Има много проучвания, които потвърждават този общ принцип, има безброй дизайнерски модели, чиято основна цел е да приложат по един или друг начин скриването на данни. За да направите програмите си по -предсказуеми, опитайте се да премахнете възможно най -много променящото се състояние.

В еднопоточна серийна програма вероятността от повреда на данни е правопропорционална на броя на компонентите, които могат да променят данните.

По правило не е възможно напълно да се отървете от глобалното състояние, но разработчикът има много ефективни инструменти в своя арсенал, които ви позволяват стриктно да контролирате кои компоненти на програмата могат да променят състоянието. Освен това научихме как да създаваме рестриктивни API слоеве около примитивни структури от данни. Следователно имаме добър контрол върху това как тези структури от данни се променят.

Проблемите на глобално променящото се състояние постепенно стават очевидни в края на 80-те и началото на 90-те години с разпространението на програми, базирани на събития. Програмите вече не стартират „от началото“ или следват един, предвидим път на изпълнение „до края“. Съвременните програми имат начално състояние, след излизане от което в тях се случват събития - в непредсказуем ред, с променливи времеви интервали. Кодът остава еднопоточен, но вече става асинхронен. Вероятността от повреда на данни се увеличава именно защото редът на настъпване на събитията е много важен. Подобни ситуации са доста често срещани: ако събитие В се случи след събитие А, тогава всичко работи добре. Но ако събитие А се случи след събитие В и събитие С има време да се намеси между тях, тогава данните могат да бъдат изкривени до неузнаваемост.

Ако са включени паралелни потоци, проблемът се задълбочава допълнително, тъй като няколко метода могат да работят едновременно върху глобалното състояние. Става невъзможно да се прецени как точно се променя глобалното състояние. Вече говорим не само за факта, че събитията могат да възникнат в непредсказуем ред, но и за това, че състоянието на няколко нишки на изпълнение може да бъде актуализирано. едновременно... С асинхронното програмиране можете като минимум да гарантирате, че определено събитие не може да се случи, преди да е завършило обработката на друго събитие. Тоест е възможно да се каже със сигурност какво ще бъде глобалното състояние в края на обработката на определено събитие. В многопоточния код по правило е невъзможно да се определи кои събития ще се случат паралелно, така че е невъзможно със сигурност да се опише глобалното състояние във всеки момент от времето.

Многонишкова програма с широко глобално променящо се състояние е един от най -красноречивите примери за принципа на несигурността на Хайзенберг, за който знам. Невъзможно е да се провери състоянието на програма, без да се променя нейното поведение.

Когато започна още един филип за глобалното променящо се състояние (същността е очертана в предишните няколко параграфа), програмистите завъртат очи и ме уверяват, че знаят всичко това от дълго време. Но ако знаете това, защо не можете да разберете от кода си? Програмите са натъпкани с глобално променящо се състояние и програмистите се чудят защо кодът не работи.

Не е изненадващо, че най -важната работа в многонишковото програмиране се случва по време на фазата на проектиране. Изисква се ясно да се определи какво трябва да прави програмата, да се разработят независими модули за изпълнение на всички функции, да се опише подробно какви данни са необходими за кой модул и да се определят начините за обмен на информация между модулите ( Да, не забравяйте да подготвите хубави тениски за всички, участващи в проекта. Първото нещо.- прибл. изд. в оригинал). Този процес не се различава коренно от проектирането на еднопоточна програма. Ключът към успеха, както при еднопоточния код, е да се ограничат взаимодействията между модулите. Ако можете да се отървете от споделеното променящо се състояние, проблемите със споделянето на данни просто няма да възникнат.

Някой може да спори, че понякога няма време за толкова деликатен дизайн на програмата, който ще направи възможно да се направи без глобалното състояние. Вярвам, че е възможно и необходимо да отделите време за това. Нищо не засяга многонишковите програми толкова разрушително, колкото опитите да се справим с глобалното променящо се състояние. Колкото повече детайли трябва да управлявате, толкова по -вероятно е вашата програма да достигне връх и да се срине.

В реалистичните приложения трябва да има определено споделено състояние, което може да се промени. И тук повечето програмисти започват да имат проблеми. Програмистът вижда, че тук се изисква споделено състояние, обръща се към многонишковия арсенал и взема оттам най -простия инструмент: универсално заключване (критична секция, mutex или както го наричат). Изглежда те вярват, че взаимното изключване ще реши всички проблеми със споделянето на данни.

Броят на проблемите, които могат да възникнат с такава единична ключалка, е потресаващ. Трябва да се вземат предвид условията на състезанието, проблемите с прекалено голямото блокиране и проблемите с справедливостта при разпределението са само няколко примера. Ако имате няколко ключалки, особено ако са вложени, тогава ще трябва да предприемете и мерки срещу блокиране, динамично блокиране, блокиране на опашки и други заплахи, свързани с паралелността. Освен това има присъщи проблеми с единичното блокиране.
Когато пиша или преглеждам код, имам почти безпогрешно желязно правило: ако сте направили ключалка, значи изглежда сте сгрешили някъде.

Това твърдение може да се коментира по два начина:

  1. Ако имате нужда от заключване, вероятно имате глобално променящо се състояние, което искате да защитите от едновременни актуализации. Наличието на глобално променящо се състояние е недостатък във фазата на проектиране на приложението. Преглед и редизайн.
  2. Използването на ключалки правилно не е лесно и може да бъде изключително трудно да се локализират грешки, свързани със заключването. Много е вероятно да използвате ключалката неправилно. Ако видя заключване и програмата се държи по необичаен начин, тогава първото нещо, което правя, е да проверя кода, който зависи от заключването. И обикновено намирам проблеми в това.

И двете интерпретации са правилни.

Писането на многонишков код е лесно. Но е много, много трудно да се използват правилно примитивите за синхронизация. Може би не сте квалифицирани да използвате правилно дори една ключалка. В края на краищата ключалките и другите примитиви за синхронизация са конструкции, които се издигат на нивото на цялата система. Хората, които разбират паралелното програмиране много по-добре от вас, използват тези примитиви за изграждане на едновременни структури от данни и конструкции за синхронизация на високо ниво. А вие и аз, обикновени програмисти, просто приемаме такива конструкции и ги използваме в нашия код. Приложният програмист не трябва да използва примитиви за синхронизация на ниско ниво по-често, отколкото прави директни повиквания към драйверите на устройства. Тоест, почти никога.

Опитът да се използват ключалки за решаване на проблеми със споделянето на данни е като гасене на огън с течен кислород. Подобно на пожар, подобни проблеми са по -лесни за предотвратяване, отколкото за отстраняване. Ако се отървете от споделеното състояние, не е нужно да злоупотребявате и с примитивите за синхронизация.

Повечето от това, което знаете за многопоточността, е без значение

В многопоточните уроци за начинаещи ще научите какво представляват нишките. След това авторът ще започне да обмисля различни начини, по които тези нишки могат да работят паралелно - например да говорим за контролиране на достъпа до споделени данни с помощта на ключалки и семафори, да се спрем на това, което може да се случи при работа със събития. Ще разгледа отблизо променливите на състоянието, бариерите на паметта, критичните секции, мютексите, летливите полета и атомните операции. Ще бъдат разгледани примери за това как да се използват тези конструкции на ниско ниво за извършване на всякакви системни операции. След като прочете наполовина този материал, програмистът решава, че вече знае достатъчно за всички тези примитиви и тяхната употреба. В крайна сметка, ако знам как работи това нещо на системно ниво, мога да го приложа по същия начин на ниво приложение. Да?

Представете си да кажете на тийнейджър как сам да сглоби двигател с вътрешно горене. След това, без никакво обучение по шофиране, го качвате зад волана на кола и казвате: „Върви!“ Тийнейджърът разбира как работи колата, но няма представа как да стигне от точка А до точка Б на нея.

Разбирането как нишките работят на системно ниво обикновено не помага по никакъв начин на ниво приложение. Не предполагам, че програмистите не трябва да научават всички тези подробности на ниско ниво. Просто не очаквайте да можете да приложите тези знания веднага, когато проектирате или разработвате бизнес приложение.

Уводната литература за нишки (и свързаните с нея академични курсове) не трябва да изследва такива конструкции на ниско ниво. Трябва да се съсредоточите върху решаването на най-често срещаните класове проблеми и да покажете на разработчиците как тези проблеми се решават с помощта на възможности на високо ниво. По принцип повечето бизнес приложения са изключително прости програми. Те четат данни от едно или повече устройства за въвеждане, извършват сложна обработка на тези данни (например в процеса те изискват още данни) и след това извеждат резултатите.

Често такива програми се вписват идеално в модела доставчик-потребител, който изисква само три нишки:

  • входният поток чете данните и ги поставя на входната опашка;
  • работната нишка чете записи от входната опашка, обработва ги и поставя резултатите в изходната опашка;
  • изходният поток чете записи от изходната опашка и ги съхранява.

Тези три нишки работят независимо, комуникацията между тях се осъществява на ниво опашка.

Докато технически тези опашки могат да се разглеждат като зони на споделено състояние, на практика те са просто комуникационни канали, в които работи тяхната собствена вътрешна синхронизация. Опашките поддържат работа с много производители и потребители едновременно, можете да добавяте и премахвате елементи в тях паралелно.

Тъй като етапите на въвеждане, обработка и изход са изолирани един от друг, тяхното изпълнение може лесно да се промени, без да се засяга останалата част от програмата. Докато типът данни в опашката не се променя, можете да рефакторирате отделни компоненти на програмата по ваша преценка. Освен това, тъй като в опашката участват произволен брой доставчици и потребители, не е трудно да се добавят други производители / потребители. Можем да имаме десетки входни потоци, които записват информация в една и съща опашка, или десетки работни нишки, които вземат информация от входната опашка и усвояват данните. В рамките на един компютър такъв модел се мащабира добре.

Най-важното е, че съвременните езици за програмиране и библиотеки правят много лесно създаването на приложения производител-потребител. В .NET ще намерите паралелни колекции и библиотеката за потоци от данни TPL. Java има услугата Executor, както и BlockingQueue и други класове от пространството на имената java.util.concurrent. C ++ има библиотека за нишки Boost и библиотека на Intel Thread Building Blocks. Visual Studio 2013 на Microsoft представя асинхронни агенти. Подобни библиотеки се предлагат и на Python, JavaScript, Ruby, PHP и, доколкото знам, на много други езици. Можете да създадете приложение производител-потребител, използвайки някой от тези пакети, без никога да се налага да прибягвате до заключвания, семафори, променливи на условията или други примитиви за синхронизация.

В тези библиотеки свободно се използват голямо разнообразие от примитиви за синхронизация. Това е добре. Всички тези библиотеки са написани от хора, които разбират многопоточността несравнимо по -добре от обикновения програмист. Работата с такава библиотека е практически същата като използването на езикова библиотека по време на работа. Това може да се сравни с програмирането на език на високо ниво, а не на асемблер.

Моделът доставчик-потребител е само един от многото примери. Горните библиотеки съдържат класове, които могат да се използват за внедряване на много от общите шаблони за проектиране на нишки, без да навлизат в подробности на ниско ниво. Възможно е да се създават мащабни многонишкови приложения, без да се притеснявате как нишките са координирани и синхронизирани.

Работете с библиотеки

Така че създаването на многопоточни програми не се различава коренно от писането на еднопоточни синхронни програми. Важните принципи на капсулиране и скриване на данни са универсални и придобиват значение само когато са включени множество едновременни нишки. Ако пренебрегнете тези важни аспекти, дори и най-изчерпателните познания за нишки на ниско ниво няма да ви спасят.

Съвременните разработчици трябва да решават много проблеми на ниво приложно програмиране, случва се просто да няма време да се мисли за случващото се на системно ниво. Колкото по -сложни стават приложенията, толкова по -сложните детайли трябва да бъдат скрити между нивата на API. Правим това повече от дузина години. Може да се твърди, че качественото скриване на сложността на системата от програмиста е основната причина програмистът да може да пише съвременни приложения. В този смисъл, не крием ли сложността на системата чрез внедряване на контура за съобщения на потребителския интерфейс, изграждане на комуникационни протоколи на ниско ниво и т.н.?

Подобна е ситуацията и с многопоточността. Повечето от многопоточните сценарии, които средният програмист на бизнес приложения може да срещне, вече са добре известни и добре внедрени в библиотеките. Библиотечните функции вършат чудесна работа, като скриват огромната сложност на паралелизма. Трябва да се научите как да използвате тези библиотеки по същия начин, по който използвате библиотеки с елементи на потребителския интерфейс, комуникационни протоколи и много други инструменти, които просто работят. Оставете многонишковото ниско ниво на специалистите - авторите на библиотеките, използвани при създаването на приложения.

NS Тази статия не е за опитни укротители на Python, за които разплитането на тази топка змии е детска игра, а по -скоро повърхностен преглед на многонишковите възможности за новозависимия питон.

За съжаление, няма толкова много материали на руски език по темата за многопоточността в Python, а питоните, които не бяха чували нищо, например за GIL, започнаха да ми се натъкват със завидна редовност. В тази статия ще се опитам да опиша най -основните характеристики на многонишков питон, ще ви кажа какво е GIL и как да живеем с него (или без него) и много други.


Python е очарователен език за програмиране. Той перфектно съчетава много програмни парадигми. Повечето задачи, които програмистът може да изпълни, се решават лесно, елегантно и сбито. Но за всички тези проблеми често е достатъчно еднопоточно решение, а еднопоточните програми обикновено са предвидими и лесни за отстраняване на грешки. Същото не може да се каже за многонишкови и многопроцесорни програми.

Многонишкови приложения


Python има модулрезба , и има всичко необходимо за многопоточно програмиране: има различни видове заключване, семафор и механизъм за събития. С една дума - всичко необходимо за по -голямата част от многопоточните програми. Освен това използването на всички тези инструменти е доста просто. Нека разгледаме пример за програма, която стартира 2 нишки. Едната нишка пише десет "0", другата - десет "1" истрого на свой ред.

импортиране на нишки

def писател

за i in xrange (10):

печат x

Event_for_set.set ()

# инициални събития

e1 = threading.Event ()

e2 = threading.Event ()

# инициални нишки

0, e1, e2))

1, e2, e1))

# начални нишки

t1.start ()

t2.start ()

t1.join ()

t2.join ()


Без магия или вуду код. Кодът е ясен и последователен. Освен това, както можете да видите, ние създадохме поток от функция. Това е много удобно за малки задачи. Този код също е доста гъвкав. Да предположим, че имаме трети процес, който пише „2“, тогава кодът ще изглежда така:

импортиране на нишки

def писател (x, event_for_wait, event_for_set):

за i in xrange (10):

Event_for_wait.wait () # чакане за събитие

Event_for_wait.clear () # чисто събитие за бъдещето

печат x

Event_for_set.set () # зададено събитие за съседна нишка

# инициални събития

e1 = threading.Event ()

e2 = threading.Event ()

e3 = threading.Event ()

# инициални нишки

t1 = threading.Thread (target = писател, args = ( 0, e1, e2))

t2 = threading.Thread (target = писател, args = ( 1, e2, e3))

t3 = threading.Thread (target = писател, args = ( 2, e3, e1))

# начални нишки

t1.start ()

t2.start ()

t3.start ()

e1.set () # инициира първото събитие

# присъединете нишки към основната нишка

t1.join ()

t2.join ()

t3.join ()


Добавихме ново събитие, нова нишка и леко променихме параметрите, с които
стартират потоци (разбира се, можете да напишете по -общо решение, като използвате например MapReduce, но това е извън обхвата на тази статия).
Както можете да видите, все още няма магия. Всичко е просто и ясно. Да отидем по -нататък.

Глобално заключване на преводача


Има две най -често срещани причини за използване на нишки: първо, за да се повиши ефективността от използването на многоядрената архитектура на съвременните процесори, а оттам и производителността на програмата;
второ, ако трябва да разделим логиката на програмата на паралелни, напълно или частично асинхронни секции (например, за да можем да пингваме няколко сървъра едновременно).

В първия случай се сблъскваме с такова ограничение на Python (или по -скоро неговата основна CPython реализация) като Global Interpreter Lock (или GIL за кратко). Концепцията на GIL е, че само една нишка може да се изпълнява от процесор наведнъж. Това се прави, за да няма борба между нишките за отделни променливи. Изпълнимата нишка получава достъп до цялата среда. Тази функция на внедряването на нишки в Python значително опростява работата с нишки и дава известна безопасност на нишката.

Но има един тънък момент: може да изглежда, че многопоточното приложение ще работи точно същото време като еднопоточното приложение, което прави същото, или сумата от времето за изпълнение на всяка нишка на процесора. Но тук ни очаква един неприятен ефект. Помислете за програмата:

с отворен ("test1.txt", "w") като fout:

за i in xrange (1000000):

печат >> fout, 1


Тази програма просто записва милион реда „1“ във файл и го прави за ~ 0,35 секунди на моя компютър.

Помислете за друга програма:

от резба импортиране Нишка

def write (име на файл, n):

с отворен (име на файл, "w") като fout:

за i в xrange (n):

печат >> fout, 1

t1 = Нишка (target = писател, args = ("test2.txt", 500000,))

t2 = Нишка (target = писател, args = ("test3.txt", 500000,))

t1.start ()

t2.start ()

t1.join ()

t2.join ()


Тази програма създава 2 нишки. Във всяка нишка той записва в отделен файл половин милион реда "1". Всъщност количеството работа е същото като в предишната програма. Но с течение на времето тук се получава интересен ефект. Програмата може да работи от 0,7 секунди до цели 7 секунди. Защо се случва това?

Това се дължи на факта, че когато нишката не се нуждае от ресурс на процесора, тя освобождава GIL и в този момент може да се опита да я получи и друга нишка, а също и основната нишка. В същото време операционната система, знаейки, че има много ядра, може да утежни всичко, като се опита да разпредели нишки между ядрата.

UPD: в момента в Python 3.2 има подобрена реализация на GIL, при която този проблем е частично решен, по -специално поради факта, че всяка нишка, след като загуби контрол, чака кратък период от време преди това може отново да заснеме GIL (има добра презентация на английски)

„Значи не можете да пишете ефективни многонишкови програми в Python?“ - питате. Не, разбира се, има изход и дори няколко.

Многопроцесорни приложения


За да реши в известен смисъл проблема, описан в предишния параграф, Python има модулподпроцес ... Можем да напишем програма, която искаме да изпълним в паралелна нишка (всъщност вече процес). И го стартирайте в една или повече нишки в друга програма. Това наистина би ускорило нашата програма, тъй като нишките, създадени в стартера GIL, не се вдигат, а само чакат завършването на текущия процес. Този метод обаче има много проблеми. Основният проблем е, че става трудно да се прехвърлят данни между процесите. Ще трябва по някакъв начин да сериализирате обекти, да установите комуникация чрез PIPE или други инструменти, но всичко това неизбежно носи допълнителни разходи и кодът става труден за разбиране.

Тук може да ни помогне друг подход. Python има многопроцесорен модул ... По отношение на функционалността този модул приличарезба ... Например процесите могат да се създават по същия начин от обикновени функции. Методите за работа с процеси са почти същите като за нишки от модула за нишки. Но за синхронизиране на процеси и обмен на данни е обичайно да се използват други инструменти. Говорим за опашки (Queue) и тръби (Pipe). Тук обаче има и аналози на ключалки, събития и семафори, които бяха в нишката.

В допълнение, многопроцесорният модул има механизъм за работа със споделена памет. За тази цел модулът има класове на променлива (стойност) и масив (масив), които могат да бъдат „споделени“ между процесите. За удобство при работа със споделени променливи можете да използвате класовете мениджър. Те са по -гъвкави и по -лесни за използване, но по -бавни. Трябва да се отбележи, че има хубава възможност да се правят общи типове от модула ctypes с помощта на модула multiprocessing.sharedctypes.

Също така в многопроцесорния модул има механизъм за създаване на пулове от процеси. Този механизъм е много удобен за използване за прилагане на модела Master-Worker или за прилагане на паралелна карта (която в известен смисъл е специален случай на Master-Worker).

От основните проблеми при работата с многопроцесорния модул си струва да се отбележи относителната зависимост на този модул от платформата. Тъй като работата с процеси е организирана по различен начин в различните операционни системи, някои ограничения са наложени върху кода. Например Windows няма механизъм на вилица, така че точката за разделяне на процеса трябва да бъде опакована в:

ако __name__ == "__main__":


Този дизайн обаче вече е добра форма.

Какво друго...


Има и други библиотеки и подходи за писане на паралелни приложения в Python. Например можете да използвате Hadoop + Python или различни реализации на Python MPI (pyMPI, mpi4py). Можете дори да използвате обвивки на съществуващи библиотеки на C ++ или Fortran. Тук може да се споменат такива рамки / библиотеки като Pyro, Twisted, Tornado и много други. Но всичко това вече излиза извън обхвата на тази статия.

Ако моят стил ви е харесал, тогава в следващата статия ще се опитам да ви кажа как да пишете прости преводачи в PLY и за какво могат да се използват.

Глава 10.

Многонишкови приложения

Многозадачността в съвременните операционни системи се приема за даденост [ Преди появата на Apple OS X нямаше съвременни многозадачни операционни системи на компютри Macintosh. Много е трудно да се проектира правилно операционна система с пълна многозадачност, така че OS X трябваше да се основава на Unix.]. Потребителят очаква, че когато текстовият редактор и пощенският клиент се стартират едновременно, тези програми няма да противоречат, а при получаване на електронна поща редакторът няма да спре да работи. Когато се стартират няколко програми едновременно, операционната система бързо превключва между програмите, като им осигурява процесор на свой ред (освен ако, разбира се, на компютъра не са инсталирани множество процесори). Като резултат, илюзиястартиране на няколко програми едновременно, защото дори най -добрият машинописец (и най -бързата интернет връзка) не може да се справи с модерен процесор.

Многопоточността в известен смисъл може да се разглежда като следващото ниво на многозадачност: вместо превключване между различни програми,операционната система превключва между различни части на една и съща програма. Например, многопоточен имейл клиент ви позволява да получавате нови имейл съобщения, докато четете или съставяте нови съобщения. В наши дни многопоточността също се приема за даденост от много потребители.

VB никога не е имал нормална поддръжка за многопоточност. Вярно е, че един от неговите сортове се появи във VB5 - съвместен стрийминг модел(резба на апартамент). Както ще видите скоро, моделът за съвместна работа предоставя на програмиста някои от предимствата на многопоточността, но не се възползва напълно от всички функции. Рано или късно трябва да преминете от тренировъчна машина към истинска и VB .NET стана първата версия на VB с поддръжка на безплатен многонишков модел.

Многопоточността обаче не е една от функциите, които лесно се внедряват в езиците за програмиране и лесно се усвояват от програмистите. Защо?

Тъй като в многонишкови приложения могат да възникнат много сложни грешки, които се появяват и изчезват непредсказуемо (и такива грешки са най -трудни за отстраняване на грешки).

Честно предупреждение: многопоточността е една от най -трудните области на програмиране. Най -малкото невнимание води до появата на неуловими грешки, чието коригиране отнема астрономически суми. По тази причина тази глава съдържа много лошопримери - умишлено ги написахме по такъв начин, че да демонстрираме често срещани грешки. Това е най -сигурният подход към изучаването на многонишково програмиране: трябва да можете да забележите потенциални проблеми, когато на пръв поглед изглежда, че всичко работи добре, и да знаете как да ги решите. Ако искате да използвате многопоточни техники за програмиране, не можете без него.

Тази глава ще постави солидна основа за по -нататъшна независима работа, но няма да можем да опишем многонишковото програмиране във всички тънкости - само отпечатаната документация за класовете на пространството от имена Threading заема повече от 100 страници. Ако искате да овладеете многонишковото програмиране на по -високо ниво, вижте специализирани книги.

Но колкото и опасно да е многонишковото програмиране, то е незаменимо за професионално решаване на някои проблеми. Ако програмите ви не използват многопоточност, където е подходящо, потребителите ще бъдат много разочаровани и ще предпочетат друг продукт. Например, само в четвъртата версия на популярната програма за електронна поща Eudora се появиха многонишки възможности, без които е невъзможно да си представим някоя съвременна програма за работа с електронна поща. По времето, когато Eudora въведе поддръжка за многопоточност, много потребители (включително един от авторите на тази книга) бяха преминали към други продукти.

И накрая, в .NET еднопоточните програми просто не съществуват. Всичко.NET програмите са многонишкови, защото събирачът на боклук работи като фонов процес с нисък приоритет. Както е показано по -долу, за сериозно графично програмиране в .NET, правилното нишене може да предотврати блокирането на графичния интерфейс, когато програмата изпълнява продължителни операции.

Представяне на многопоточност

Всяка програма работи в определена контекст,описващи разпределението на код и данни в паметта. Чрез запазване на контекста състоянието на програмния поток всъщност се запазва, което ви позволява да го възстановите в бъдеще и да продължите изпълнението на програмата.

Запазването на контекста идва с разходи във времето и паметта. Операционната система запомня състоянието на програмната нишка и прехвърля контрола към друга нишка. Когато програмата иска да продължи изпълнението на спряната нишка, запазеният контекст трябва да бъде възстановен, което отнема още повече време. Следователно, многопоточността трябва да се използва само когато ползите компенсират всички разходи. Някои типични примери са изброени по -долу.

  • Функционалността на програмата е ясно и естествено разделена на няколко разнородни операции, както в примера с получаване на електронна поща и подготовка на нови съобщения.
  • Програмата извършва дълги и сложни изчисления и не искате графичният интерфейс да бъде блокиран по време на изчисленията.
  • Програмата работи на многопроцесорен компютър с операционна система, която поддържа използването на множество процесори (стига броят на активните нишки да не надвишава броя на процесорите, паралелното изпълнение на практика е свободно от разходите, свързани с превключване на нишки).

Преди да преминем към механиката на многонишковите програми, е необходимо да посочим едно обстоятелство, което често предизвиква объркване сред начинаещите в областта на многонишкото програмиране.

В програмния поток се изпълнява процедура, а не обект.

Трудно е да се каже какво се има предвид под израза „обектът се изпълнява“, но един от авторите често преподава семинари за многонишково програмиране и този въпрос се задава по -често от други. Може би някой смята, че работата на програмната нишка започва с извикване на метода New на класа, след което нишката обработва всички съобщения, предадени на съответния обект. Такива представи абсолютногрешат. Един обект може да съдържа няколко нишки, които изпълняват различни (а понякога дори и едни и същи) методи, докато съобщенията на обекта се предават и получават от няколко различни нишки (между другото, това е една от причините, които усложняват многопоточното програмиране: за да отстраните грешки в програма, трябва да разберете коя нишка в даден момент изпълнява тази или онази процедура!).

Тъй като нишките се създават от методи на обекти, самият обект обикновено се създава преди нишката. След успешно създаване на обекта, програмата създава нишка, предавайки й адреса на метода на обекта и едва след товадава заповед да започне изпълнението на нишката. Процедурата, за която е създадена нишката, както всички процедури, може да създава нови обекти, да извършва операции върху съществуващи обекти и да извиква други процедури и функции, които са в нейния обхват.

Общите методи на класове могат също да се изпълняват в програмни нишки. В този случай имайте предвид и друго важно обстоятелство: нишката завършва с излизане от процедурата, за която е създадена. Нормалното завършване на програмния поток е невъзможно, докато процедурата не бъде излязла.

Нишките могат да завършат не само естествено, но и ненормално. Това обикновено не се препоръчва. Вижте Прекратяване и прекъсване на потоци за повече информация.

Основните .NET инструменти, свързани с използването на програмни нишки, са концентрирани в пространството на имената Threading. Следователно повечето многонишкови програми трябва да започват със следния ред:

Импортира система

Импортирането на пространство от имена прави вашата програма по -лесна за въвеждане и активира технологията IntelliSense.

Директната връзка на потоците с процедури предполага, че в тази картина, делегати(виж глава 6). По -конкретно, пространството с имена Threading включва делегата на ThreadStart, който обикновено се използва при стартиране на програмни нишки. Синтаксисът за използване на този делегат изглежда така:

Публичен делегат Sub ThreadStart ()

Кодът, извикан с делегата на ThreadStart, не трябва да има параметри или връщаща стойност, така че нишките не могат да бъдат създадени за функции (които връщат стойност) и за процедури с параметри. За да прехвърлите информация от потока, вие също трябва да потърсите алтернативни средства, тъй като изпълнените методи не връщат стойности и не могат да използват трансфер чрез препратка. Например, ако ThreadMethod е в класа WilluseThread, тогава ThreadMethod може да комуникира информация чрез промяна на свойствата на екземплярите на класа WillUseThread.

Приложни домейни

.NET нишките се изпълняват в така наречените домейни на приложения, дефинирани в документацията като "пясъчната кутия, в която се изпълнява приложението". Домейнът на приложение може да се разглежда като олекотена версия на процесите на Win32; един -единствен процес на Win32 може да съдържа множество приложни домейни. Основната разлика между приложните домейни и процеси е, че процесът на Win32 има свое собствено адресно пространство (в документацията домейните на приложенията също се сравняват с логическите процеси, изпълнявани във физически процес). В NET цялото управление на паметта се управлява от времето на изпълнение, така че множество домейни на приложения могат да се изпълняват в един процес на Win32. Едно от предимствата на тази схема е подобрените възможности за мащабиране на приложенията. Инструментите за работа с приложни домейни са в клас AppDomain. Препоръчваме ви да изучите документацията за този клас. С негова помощ можете да получите информация за средата, в която се изпълнява вашата програма. По -специално, класът AppDomain се използва при извършване на отражение върху .NET системни класове. Следващата програма изброява заредените сборки.

Импортира система.Отражение

Модул Модул

Sub Main ()

Затъмнете домейна като AppDomain

theDomain = AppDomain.CurrentDomain

Затъмнени сглобки () As

Асамблеи = theDomain.GetAssemblies

Dim anAssemblyxAs

За всяка една Сглобка В Асамблеи

Console.WriteLinetanAssembly.Full Name) Напред

Console.ReadLine ()

End Sub

Краен модул

Създаване на потоци

Нека започнем с елементарен пример. Да предположим, че искате да изпълните процедура в отделна нишка, която намалява стойността на брояча в безкраен цикъл. Процедурата е дефинирана като част от класа:

Публичен клас WillUseThreads

Public Sub SubtractFromCounter ()

Dim count As Integer

Брой Do While True - = 1

Console.WriteLlne ("Аз съм в друга нишка и брояч ="

& броя)

Цикъл

End Sub

Краен клас

Тъй като условието Do цикъл винаги е вярно, може да мислите, че нищо няма да попречи на процедурата SubtractFromCounter. В многонишко приложение обаче това не винаги е така.

Следният фрагмент показва процедурата Sub Main, която стартира нишката и командата Imports:

Опция Строго при импортиране на система, модул за нишка модул

Sub Main ()

1 Dim myTest As New WillUseThreads ()

2 Затъмнете bThreadStart като нов ThreadStart (AddressOf _

myTest.SubtractFromCounter)

3 Затъмнете bThread като нова нишка (bThreadStart)

4 "bThread.Start ()

Dim i As Integer

5 Правете, докато е вярно

Console.WriteLine ("В основната нишка и броят е" & i) i + = 1

Цикъл

End Sub

Краен модул

Нека да разгледаме най -важните точки последователно. На първо място, процедурата Sub Man n винаги работи основен поток(основна нишка). В .NET програмите винаги работят поне две нишки: основната нишка и нишката за събиране на боклука. Ред 1 създава нов екземпляр на тестовия клас. В ред 2 създаваме делегат на ThreadStart и предаваме адреса на процедурата SubtractFromCounter на екземпляра на тестовия клас, създаден в ред 1 (тази процедура се извиква без параметри). добреЧрез импортиране на пространството от имена Threading, дългото име може да бъде пропуснато. Новият обект на нишка е създаден на ред 3. Забележете преминаването на делегата на ThreadStart при извикване на конструктора на клас Thread. Някои програмисти предпочитат да обединят тези два реда в един логически ред:

Затъмнете bThread като нова нишка (New ThreadStarttAddressOf _

myTest.SubtractFromCounter))

И накрая, ред 4 "стартира" нишката, като извика метода Start на екземпляра Thread, създаден за делегата на ThreadStart. Извиквайки този метод, ние казваме на операционната система, че процедурата Subtract трябва да работи в отделна нишка.

Думата "стартира" в предишния параграф е затворена в кавички, защото това е една от многото странности на многонишкото програмиране: Извикването на Start всъщност не стартира нишката! Той само казва на операционната система да планира изпълнението на посочената нишка, но това е извън контрола на програмата за директно стартиране. Няма да можете да стартирате самостоятелно изпълнение на нишки, защото операционната система винаги контролира изпълнението на нишки. В по -късен раздел ще научите как да използвате приоритета, за да накарате операционната система да стартира вашата нишка по -бързо.

На фиг. 10.1 показва пример за това какво може да се случи след стартиране на програма и след това да се прекъсне с клавиша Ctrl + Break. В нашия случай нова нишка започна едва след като броячът в основната нишка се увеличи до 341!

Ориз. 10.1. Просто изпълнение на многопоточен софтуер

Ако програмата работи за по -дълъг период от време, резултатът ще изглежда нещо подобно на показаното на фиг. 10.2. Виждаме, че тизавършването на текущата нишка се преустановява и контролът се прехвърля отново към основната нишка. В този случай има проява изпреварващо многонишково срязване през времето.Значението на този ужасяващ термин е обяснено по -долу.

Ориз. 10.2. Превключване между нишки в проста многопоточна програма

При прекъсване на нишки и прехвърляне на контрол към други нишки, операционната система използва принципа на превантивно многонишково нишене чрез нарязване на време. Квантоването на време решава и един от често срещаните проблеми, възникнали преди в многонишковите програми - една нишка заема цялото време на процесора и не отстъпва на контрола на други нишки (като правило това се случва в интензивни цикли като този по -горе). За да предотвратите отвличането на изключителен процесор, вашите нишки трябва да прехвърлят контрола върху други нишки от време на време. Ако програмата се окаже „в безсъзнание“, има друго, малко по -малко желано решение: операционната система винаги изпреварва работеща нишка, независимо от нейното ниво на приоритет, така че достъпът до процесора се предоставя на всяка нишка в системата.

Тъй като схемите за квантуване на всички версии на Windows, които изпълняват .NET, имат минимален интервал от време за всяка нишка, в програмирането .NET, проблемите с изключителните грайфери на процесора не са толкова сериозни. От друга страна, ако .NET рамката някога бъде адаптирана за други системи, това може да се промени.

Ако включим следния ред в програмата си преди да извикаме Start, тогава дори нишките с най -нисък приоритет ще получат част от времето на процесора:

bThread.Priority = ThreadPriority.Highest

Ориз. 10.3. Нишката с най -висок приоритет обикновено се стартира по -бързо

Ориз. 10.4. Процесорът е предвиден и за ниски приоритети

Командата присвоява максималния приоритет на новата нишка и намалява приоритета на основната нишка. Фиг. 10.3 може да се види, че новата нишка започва да работи по -бързо от преди, но, както е показано на фиг. 10.4, основната нишка също получава контролмързел (макар и за много кратко време и само след продължителна работа на потока с изваждане). Когато стартирате програмата на компютрите си, ще получите резултати, подобни на тези, показани на фиг. 10.3 и 10.4, но поради различията между нашите системи няма да има точно съвпадение.

Изброеният тип ThreadPrlority включва стойности за пет нива на приоритет:

Приоритет на нишката. Най -висок

ThreadPriority.AboveNormal

ThreadPrlority.Normal

ThreadPriority.BelowNormal

ThreadPriority.Lowest

Метод на присъединяване

Понякога програмната нишка трябва да бъде поставена на пауза, докато друга нишка завърши. Да предположим, че искате да поставите на пауза нишка 1, докато нишка 2 завърши изчислението си. За това от поток 1методът Join се извиква за поток 2. С други думи, командата

thread2.Join ()

спира текущата нишка и изчаква нишката да завърши. Нишка 1 отива към заключено състояние.

Ако се присъедините към поток 1 към поток 2 чрез метода Join, операционната система автоматично ще стартира поток 1 след поток 2. Обърнете внимание, че процесът на стартиране е недетерминиран:не е възможно да се каже точно колко време след края на нишка 2, ще започне да работи нишка 1. Има друга версия на Join, която връща булева стойност:

thread2.Join (Integer)

Този метод или изчаква нишката 2 да завърши, или деблокира нишка 1 след изтичане на определения интервал от време, което води до планиране на операционната система, за да разпредели отново времето на процесора за нишката. Методът връща True, ако нишка 2 приключи преди изтичането на определения интервал на изчакване, и False в противен случай.

Запомнете основното правило: независимо дали нишката 2 е завършила или изтекла, нямате контрол върху това кога е активирана нишка 1.

Имена на теми, CurrentThread и ThreadState

Свойството Thread.CurrentThread връща препратка към обекта на нишката, който се изпълнява в момента.

Въпреки че има прекрасен прозорец с нишки за отстраняване на грешки в многонишкови приложения във VB .NET, който е описан по -долу, много често ни помагаше командата

MsgBox (Thread.CurrentThread.Name)

Често се оказва, че кодът се изпълнява в съвсем различна нишка, от която е трябвало да се изпълнява.

Припомнете си, че терминът „недетерминирано планиране на програмни потоци“ означава много просто нещо: програмистът практически няма на разположение средства да влияе върху работата на планиращия. Поради тази причина програмите често използват свойството ThreadState, което връща информация за текущото състояние на нишка.

Прозорец за потоци

Прозорецът Threads на Visual Studio .NET е безценен при отстраняване на грешки в многонишкови програми. Активира се чрез командата Debug> Windows подменю в режим на прекъсване. Да предположим, че сте присвоили име на нишката на bThread със следната команда:

bThread.Name = "Изваждане на нишка"

Приблизителен изглед на прозореца на потоците след прекъсване на програмата с комбинацията от клавиши Ctrl + Break (или по друг начин) е показан на фиг. 10.5.

Ориз. 10.5. Прозорец за потоци

Стрелката в първата колона маркира активната нишка, върната от свойството Thread.CurrentThread. Колоната ID съдържа числови идентификатори на нишки. Следващата колона изброява имената на потоците (ако са зададени). Колоната Location показва процедурата за изпълнение (например процедурата WriteLine на класа Console на Фигура 10.5). Останалите колони съдържат информация за приоритетни и спряни нишки (вижте следващия раздел).

Прозорецът с нишки (не операционната система!) Позволява ви да контролирате нишките на вашата програма, като използвате контекстните менюта. Например, можете да спрете текущата нишка, като щракнете с десния бутон върху съответния ред и изберете командата Freeze (по-късно спрената нишка може да бъде възобновена). Спирането на нишки често се използва при отстраняване на грешки, за да се предотврати смущаването на неправилно работеща нишка в приложението. В допълнение, прозорецът потоци ви позволява да активирате друг (не спрян) поток; за да направите това, щракнете с десния бутон върху необходимия ред и изберете командата Switch To Thread от контекстното меню (или просто щракнете двукратно върху реда на нишката). Както ще бъде показано по -долу, това е много полезно за диагностициране на потенциални задънени улици.

Спиране на поток

Временно неизползваните потоци могат да бъдат прехвърлени в пасивно състояние с помощта на метода Slеer. Пасивен поток също се счита за блокиран. Разбира се, когато нишката бъде поставена в пасивно състояние, останалите нишки ще имат повече процесорни ресурси. Стандартният синтаксис на метода Slеer е следният: Thread.Sleep (interval_in_milliseconds)

В резултат на повикването Sleep активната нишка става пасивна поне за определен брой милисекунди (обаче не се гарантира активиране веднага след изтичане на посочения интервал). Моля, обърнете внимание: при извикване на метода не се препраща препратка към конкретна нишка - методът Sleep се извиква само за активната нишка.

Друга версия на Sleep кара текущата нишка да се откаже от останалото разпределено CPU време:

Thread.Sleep (0)

Следващата опция поставя текущата нишка в пасивно състояние за неограничено време (активирането става само когато извикате Interrupt):

Thread.Slеer (Timeout.Infinite)

Тъй като пасивните нишки (дори с неограничен таймаут) могат да бъдат прекъснати чрез метода Interrupt, което води до иницииране на ThreadlnterruptExcepti по изключение, обаждането на Slayer винаги е затворено в блок Try-Catch, както в следния фрагмент:

Опитвам

Thread.Sleep (200)

„Пасивното състояние на нишката е прекъснато

Хванете e като изключение

„Други изключения

Край Опитайте

Всяка програма .NET работи в програмна нишка, така че методът Sleep се използва и за спиране на програми (ако пространството на имената Threadipg не е импортирано от програмата, трябва да използвате напълно квалифицираното име Threading.Thread. Sleep).

Прекратяване или прекъсване на програмни нишки

Потокът автоматично ще се прекрати, когато методът е посочен при създаването на делегата на ThreadStart, но понякога е необходимо да се прекрати метода (и следователно нишката), когато възникнат определени фактори. В такива случаи потоците обикновено проверяват условна променлива,в зависимост от състоянието на коетосе взема решение за авариен изход от потока. Обикновено в процедурата за това е включен цикъл Do-While:

Sub ThreadedMethod ()

„Програмата трябва да предоставя средства за проучването

"условна променлива.

„Например условна променлива може да бъде оформена като свойство

Do While conditionVariable = False And MoreWorkToDo

„Основният код

Loop End Sub

Отнема известно време за проучване на условната променлива. Трябва да използвате постоянно изпитване само в цикъл, ако чакате нишката да се прекрати преждевременно.

Ако променливата на условието трябва да бъде проверена на определено място, използвайте командата If-Then заедно с Exit Sub в безкраен цикъл.

Достъпът до условна променлива трябва да бъде синхронизиран, така че експозицията от други нишки да не пречи на нормалното й използване. Тази важна тема е разгледана в раздела „Отстраняване на неизправности: Синхронизация“.

За съжаление, кодът на пасивни (или блокирани по друг начин) нишки не се изпълнява, така че опцията с опросване на условна променлива не е подходяща за тях. В този случай извикайте метода Interrupt на променливата на обекта, който съдържа препратка към желаната нишка.

Методът Interrupt може да бъде извикан само за нишки в състояние Wait, Sleep или Join. Ако извикате Interrupt за нишка, която е в едно от изброените състояния, след известно време нишката ще започне да работи отново и средата за изпълнение ще инициира ThreadlnterruptExcepti при изключение в нишката. Това се случва дори ако нишката е направена пасивна за неопределено време чрез извикване на Thread.Sleepdimeout. Безкраен). Казваме „след известно време“, защото планирането на нишките е недетерминирано. Изключението ThreadlnterruptExcepti се улавя от секцията Catch, която съдържа изходния код от състоянието на изчакване. Разделът Catch обаче не трябва да прекратява нишката при повикване за прекъсване - нишката обработва изключението, както сметне за добре.

В .NET методът Interrupt може да бъде извикан дори за деблокирани нишки. В този случай нишката се прекъсва при най -близкото блокиране.

Окачване и убиване на нишки

Пространството от имена Threading съдържа други методи, които прекъсват нормалното нишкане:

  • Спиране;
  • Прекъсване.

Трудно е да се каже защо .NET включва поддръжка на тези методи - извикването на Suspend и Abort вероятно ще доведе до нестабилност на програмата. Нито един от методите не позволява нормално деинициализиране на потока. Освен това при извикване на Suspend или Abort е невъзможно да се предвиди в какво състояние нишката ще остави обектите след като бъде спряна или прекъсната.

Извикването на Прекъсване изхвърля ThreadAbortException. За да ви помогнем да разберете защо това странно изключение не трябва да се обработва в програми, ето откъс от документацията за .NET SDK:

“... Когато нишка се унищожава чрез извикване на Прекъсване, средата на изпълнение изхвърля ThreadAbortException. Това е специален вид изключение, което не може да бъде уловено от програмата. Когато това изключение е хвърлено, средата на изпълнение изпълнява всички блокове накрая, преди да прекрати нишката. Тъй като всяко действие може да се извърши в блоковете накрая, извикайте Join, за да сте сигурни, че потокът е унищожен. "

Морал: Прекъсването и спирането не се препоръчват (и ако все още не можете без Suspend, възобновете суспендираната нишка, като използвате метода Resume). Можете безопасно да прекратите нишка само чрез анкетиране на синхронизирана променлива на условието или чрез извикване на метода Interrupt, обсъден по -горе.

Фонови нишки (демони)

Някои нишки, работещи във фонов режим, автоматично спират да работят, когато други програмни компоненти спрат. По -специално, събирачът на боклук работи в една от фоновите нишки. Фоновите нишки обикновено се създават за получаване на данни, но това се прави само ако други нишки изпълняват код, който може да обработва получените данни. Синтаксис: име на поток. IsBackGround = Истина

Ако в приложението са останали само фонови нишки, приложението автоматично ще се прекрати.

По -сериозен пример: извличане на данни от HTML код

Препоръчваме да използвате потоци само когато функционалността на програмата е ясно разделена на няколко операции. Добър пример е програмата за извличане на HTML в глава 9. Нашият клас прави две неща: извличане на данни от Amazon и тяхната обработка. Това е перфектен пример за ситуация, в която многонишковото програмиране е наистина подходящо. Създаваме класове за няколко различни книги и след това анализираме данните в различни потоци. Създаването на нова нишка за всяка книга увеличава ефективността на програмата, тъй като докато една нишка получава данни (което може да изисква изчакване на сървъра на Amazon), друга нишка ще бъде заета с обработката на вече получените данни.

Многопоточната версия на тази програма работи по-ефективно от версията с една нишка само на компютър с няколко процесора или ако получаването на допълнителни данни може ефективно да се комбинира с техния анализ.

Както бе споменато по -горе, само процедури, които нямат параметри, могат да се изпълняват в нишки, така че ще трябва да направите малки промени в програмата. По -долу е основната процедура, пренаписана, за да се изключат параметрите:

Публичен под FindRank ()

m_Rank = ScrapeAmazon ()

Console.WriteLine ("ранг на" & m_Name & "Is" & GetRank)

End Sub

Тъй като няма да можем да използваме комбинираното поле за съхраняване и извличане на информация (писането на многонишкови програми с графичен интерфейс е обсъдено в последния раздел на тази глава), програмата съхранява данните от четири книги в масив, дефиницията на която започва така:

Затъмнете книгата (3.1) като низа на книгата (0.0) = "1893115992"

theBook (0.l) = "Програмиране на VB .NET" "И т.н.

Четири потока се създават в същия цикъл, в който се създават обектите на AmazonRanker:

За i = 0 до 3

Опитвам

theRanker = Нов AmazonRanker (Книгата (i.0). theBookd.1))

aThreadStart = Нов ThreadStar (AddressOf theRanker.FindRan ()

aThread = Нова нишка (aThreadStart)

aThread.Name = Книгата (i.l)

aThread.Start () Catch e As Exception

Console.WriteLine (e.Message)

Край Опитайте

Следващия

По -долу е пълният текст на програмата:

Опция Строго при импортиране System.IO Импортира System.Net

Импортира система

Модул Модул

Sub Main ()

Затъмнете книгата (3.1) като низ

theBook (0.0) = "1893115992"

theBook (0.l) = "Програмиране на VB .NET"

theBook (l.0) = "1893115291"

theBook (l.l) = "Програмиране на бази данни VB .NET"

theBook (2,0) = "1893115623"

theBook (2.1) = "Въведение на програмиста в C #."

theBook (3.0) = "1893115593"

theBook (3.1) = "Залепете платформата .Net"

Dim i As Integer

Затъмнете theRanker As = AmazonRanker

Затъмнете aThreadStart като Threading.ThreadStart

Затъмнете aThread As Threading.Thread

За i = 0 до 3

Опитвам

theRanker = Нов AmazonRankerttheBook (i.0). книгата (i.1))

aThreadStart = Нов ThreadStart (Адрес на Ranker. FindRank)

aThread = Нова нишка (aThreadStart)

aThread.Name = Книгата (i.l)

aThread.Start ()

Хванете e като изключение

Console.WriteLlnete.Message)

Край Опитайте следващия

Console.ReadLine ()

End Sub

Краен модул

Обществен клас AmazonRanker

Частен m_URL As String

Частен m_Rank като цяло число

Частно m_Name As String

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

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

m_Name = под името на края

Публичен под FindRank () m_Rank = ScrapeAmazon ()

Console.Writeline ("рангът на" & m_Name & "е"

& GetRank) End Sub

Публично свойство само за четене GetRank () As String Get

Ако m_Rank<>0 Тогава

Върнете CStr (m_Rank) Иначе

„Проблеми

Край Ако

Край Get

Крайна собственост

Публично свойство Само за четене GetName () Както String Get

Върнете m_Name

Край Get

Крайна собственост

Частна функция ScrapeAmazon () As Integer Try

Затъмнете URL адреса като нов Uri (m_URL)

Затъмнете Заявката като WebRequest

theRequest = WebRequest.Create (theURL)

Затъмнете отговора като WebResponse

theResponse = theRequest.GetResponse

Затъмнете aReader като нов StreamReader (theResponse.GetResponseStream ())

Затъмнете данните като низ

theData = aReader.ReadToEnd

Анализ на връщането (данните)

Хванете Е като изключение

Console.WriteLine (E.Message)

Console.WriteLine (E.StackTrace)

Конзола. ReadLine ()

Край Опитайте Крайна функция

Анализ на частни функции (ByVal theData As String) As Integer

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

Ранг на продажбите:") _

+ "Ранг на продажбите на Amazon.com:". Дължина

Dim temp As String

Направете, докато theData.Substring (Location.l) = "<" temp = temp

& theData.Substring (Location.l) Location + = 1 Loop

Връщане Clnt (temp)

Крайна функция

Краен клас

Многонишковите операции обикновено се използват в .NET и I / O пространства с имена, така че библиотеката .NET Framework предоставя специални асинхронни методи за тях. За повече информация относно използването на асинхронни методи при писане на многонишкови програми вижте методите BeginGetResponse и EndGetResponse на класа HTTPWebRequest.

Основна опасност (общи данни)

Досега е разглеждан единственият безопасен случай на използване на нишки - нашите потоци не са променили общите данни.Ако позволите промяната в общите данни, потенциалните грешки започват да се умножават експоненциално и става много по -трудно да се отървете от тях за програмата. От друга страна, ако забраните модифицирането на споделени данни от различни нишки, многонишковото .NET програмиране едва ли ще се различава от ограничените възможности на VB6.

Бихме искали да ви обърнем внимание на малка програма, която демонстрира възникващите проблеми, без да навлиза в излишни подробности. Тази програма симулира къща с термостат във всяка стая. Ако температурата е с 5 градуса по Фаренхайт или повече (около 2,77 градуса по Целзий) по -ниска от целевата температура, ние нареждаме отоплителната система да увеличи температурата с 5 градуса; в противен случай температурата се повишава само с 1 градус. Ако текущата температура е по -голяма или равна на зададената, не се правят промени. Регулирането на температурата във всяка стая се извършва с отделен поток със закъснение от 200 милисекунди. Основната работа се извършва със следния фрагмент:

Ако mHouse.HouseTemp< mHouse.MAX_TEMP = 5 Then Try

Thread.Sleep (200)

Хванете равенството като ThreadlnterruptException

„Пасивното чакане е прекъснато

Хванете e като изключение

„Други изключения за крайни опити

mHouse.HouseTemp + - 5 "и т.н.

По -долу е пълният изходен код на програмата. Резултатът е показан на фиг. 10.6: Температурата в къщата е достигнала 105 градуса по Фаренхайт (40,5 градуса по Целзий)!

1 Опция Строго включено

2 Импортира система

3 Модул Модул

4 Sub Main ()

5 Dim myHouse As New House (l0)

6 Конзола. ReadLine ()

7 End Sub

8 Краен модул

9 Обществена класна къща

10 Public Const MAX_TEMP As Integer = 75

11 Частно mCurTemp като цяло число = 55

12 частни стаи mRooms () като стая

13 Public Sub New (ByVal numOfRooms As Integer)

14 ReDim mRooms (numOfRooms = 1)

15 Dim i As Integer

16 Dim aThreadStart As Threading.ThreadStart

17 Затъмнете aThread As Thread

18 За i = 0 За numOfRooms -1

19 Опитайте

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

21 aThreadStart - Нов ThreadStart (AddressOf _

mRooms (i) .CheckTempInRoom)

22 aThread = Нова нишка (aThreadStart)

23 aThread.Start ()

24 Catch E като изключение

25 Console.WriteLine (E.StackTrace)

26 Краен опит

27 Напред

28 End Sub

29 Публична собственост HouseTemp () Като цяло число

тридесет. Вземи

31 Връщане на mCurTemp

32 Край на Get

33 Set (ByVal Value As Integer)

34 mCurTemp = Стойност 35 Краен набор

36 Крайна собственост

37 Краен клас

38 Обществена класна стая

39 Частен mCurTemp като цяло число

40 Частно mName As String

41 Частна къща като къща

42 Public Sub New (ByVal theHouse As House,

ByVal temp As Integer, ByVal roomName As String)

43 mHouse = The House

44 mCurTemp = темп

45 mName = име на стая

46 End Sub

47 Публична подпроверкаTempInRoom ()

48 ChangeTemperature ()

49 End Sub

50 Private Sub ChangeTemperature ()

51 Опитайте

52 Ако mHouse.HouseTemp< mHouse.MAX_TEMP - 5 Then

53 нишка (200)

54 mHouse.HouseTemp + - 5

55 Console.WriteLine ("Am in" & Me.mName & _

56 ". Текущата температура е" & mHouse.HouseTemp)

57. Elself mHouse.HouseTemp< mHouse.MAX_TEMP Then

58 нишка.сън (200)

59 mHouse.HouseTemp + = 1

60 Console.WriteLine ("Am in" & Me.mName & _

61 ". Текущата температура е" & mHouse.HouseTemp)

62 Иначе

63 Console.WriteLine ("Am in" & Me.mName & _

64 ". Текущата температура е" & mHouse.HouseTemp)

65 "Не правете нищо, температурата е нормална

66 Край Ако

67 Catch tae As ThreadlnterruptException

68 "Пасивното изчакване е прекъснато

69 Уловете д като изключение

70 "Други изключения

71 Краен опит

72 End Sub

73 Краен клас

Ориз. 10.6. Проблеми с многопоточност

Процедурата Sub Main (редове 4-7) създава "къща" с десет "стаи". Класът House задава максимална температура от 75 градуса по Фаренхайт (около 24 градуса по Целзий). Редове 13-28 определят доста сложен къща-конструктор. Ключът към разбирането на програмата са редове 18-27. Ред 20 създава друг стаен обект и препратка към домашен обект се предава на конструктора, така че стайният обект да може да се отнася към него, ако е необходимо. Редове 21-23 стартират десет потока за регулиране на температурата във всяка стая. Класът Room е дефиниран на редове 38-73. Къща coxpa справкасе съхранява в променливата mHouse в конструктора на клас Room (ред 43). Кодът за проверка и регулиране на температурата (редове 50-66) изглежда прост и естествен, но както скоро ще видите, това впечатление е измамно! Имайте предвид, че този код е опакован в блок Try-Catch, тъй като програмата използва метода Sleep.

Едва ли някой би се съгласил да живее при температури от 105 градуса по Фаренхайт (40,5 до 24 градуса по Целзий). Какво стана? Проблемът е свързан със следния ред:

Ако mHouse.HouseTemp< mHouse.MAX_TEMP - 5 Then

И се случва следното: първо, температурата се проверява чрез поток 1. Той вижда, че температурата е твърде ниска, и я повишава с 5 градуса. За съжаление, преди температурата да се повиши, поток 1 се прекъсва и контролът се прехвърля към поток 2. Поток 2 проверява същата променлива, която все още не е промененпоток 1. По този начин поток 2 също се подготвя да повиши температурата с 5 градуса, но няма време да направи това и също преминава в състояние на изчакване. Процесът продължава, докато поток 1 се активира и премине към следващата команда - повишаване на температурата с 5 градуса. Увеличението се повтаря, когато се активират всичките 10 потока и жителите на къщата ще преживеят лошо.

Решение на проблема: синхронизация

В предишната програма възниква ситуация, когато изходът на програмата зависи от реда на изпълнение на нишките. За да се отървете от него, трябва да се уверите, че командите като

Ако mHouse.HouseTemp< mHouse.MAX_TEMP - 5 Then...

се обработват напълно от активната нишка, преди да бъде прекъсната. Това свойство се нарича атомен срам -блок от код трябва да се изпълнява от всяка нишка без прекъсване, като атомна единица. Група команди, обединени в атомен блок, не могат да бъдат прекъсвани от планировчика на нишки, докато не бъде завършен. Всеки многонишков език за програмиране има свои собствени начини за осигуряване на атомност. Във VB .NET най -лесният начин да използвате командата SyncLock е да предадете обектна променлива при извикване. Направете малки промени в процедурата ChangeTemperature от предишния пример и програмата ще работи добре:

Private Sub ChangeTemperature () SyncLock (mHouse)

Опитвам

Ако mHouse.HouseTemp< mHouse.MAXJTEMP -5 Then

Thread.Sleep (200)

mHouse.HouseTemp + = 5

Console.WriteLine ("Am in" & Me.mName & _

". Текущата температура е" & mHouse.HouseTemp)

Себе си

mHouse.HouseTemp< mHouse. MAX_TEMP Then

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

Console.WriteLine ("Am in" & Me.mName & _ ". Текущата температура е" & mHouse.HomeTemp) Иначе

Console.WriteLineC "Am in" & Me.mName & _ ". Текущата температура е" & mHouse.HouseTemp)

„Не правете нищо, температурата е нормална

Край, ако уловите връзката като ThreadlnterruptException

„Пасивното чакане беше прекъснато от Catch e As Exception

„Други изключения

Край Опитайте

Прекратете SyncLock

End Sub

Кодът на блока SyncLock се изпълнява атомно. Достъпът до него от всички други нишки ще бъде затворен, докато първата нишка освободи заключването с командата End SyncLock. Ако нишка в синхронизиран блок премине в състояние на пасивно изчакване, заключването остава, докато нишката се прекъсне или възобнови.

Правилното използване на командата SyncLock поддържа вашата програмна нишка в безопасност. За съжаление, прекомерната употреба на SyncLock има отрицателно въздействие върху производителността. Синхронизирането на кода в многонишкова програма намалява скоростта на нейната работа няколко пъти. Синхронизирайте само най -необходимия код и освободете ключалката възможно най -скоро.

Базовите класове за събиране са опасни в многопоточни приложения, но .NET Framework включва безопасни за нишки версии на повечето класове за събиране. В тези класове кодът на потенциално опасни методи е затворен в блокове SyncLock. Безопасните за нишките версии на класове за събиране трябва да се използват в многонишкови програми, където е нарушена целостта на данните.

Остава да споменем, че условните променливи се изпълняват лесно с помощта на командата SyncLock. За да направите това, просто трябва да синхронизирате писането с общото булево свойство, достъпно за четене и писане, както е направено в следния фрагмент:

Състояние на публичен клас Променлива

Частен споделен шкаф като обект = нов обект ()

Private Shared mOK Както Boolean Shared

Свойство TheConditionVariable () Като логическо

Вземи

Върнете mOK

Край Get

Задайте (ByVal стойност като булева) SyncLock (шкафче)

mOK = Стойност

Прекратете SyncLock

Краен комплект

Крайна собственост

Краен клас

Клас на командите и мониторите на SyncLock

Използването на командата SyncLock включва някои тънкости, които не са показани в простите примери по -горе. Така че изборът на обект на синхронизация играе много важна роля. Опитайте да стартирате предишната програма с командата SyncLock (Me) вместо SyncLock (mHouse). Температурата отново се повишава над прага!

Не забравяйте, че командата SyncLock се синхронизира чрез обект,предадени като параметър, а не от кодовия фрагмент. Параметърът SyncLock действа като врата за достъп до синхронизирания фрагмент от други нишки. Командата SyncLock (Аз) всъщност отваря няколко различни "врати", което точно се опитвахте да избегнете със синхронизацията. Нравственост:

За да защити споделени данни в многонишко приложение, командата SyncLock трябва да синхронизира един обект наведнъж.

Тъй като синхронизацията е свързана с конкретен обект, в някои ситуации е възможно по невнимание да се заключат други фрагменти. Да предположим, че имате два синхронизирани метода, първи и втори, и двата метода са синхронизирани на обекта bigLock. Когато нишка 1 влиза в метода първа и улавя bigLock, никоя нишка няма да може да въведе метод втора, тъй като достъпът до нея вече е ограничен до нишка 1!

Функционалността на командата SyncLock може да се разглежда като подмножество от функционалността на класа Monitor. Класът Monitor е много персонализиран и може да се използва за решаване на нетривиални задачи за синхронизация. Командата SyncLock е приблизителен аналог на методите Enter и Exi t на класа Monitor:

Опитвам

Monitor.Enter (theObject) Най -накрая

Monitor.Exit (theObject)

Край Опитайте

За някои стандартни операции (увеличаване / намаляване на променлива, обмен на съдържанието на две променливи) .NET Framework предоставя клас Interlocked, чиито методи извършват тези операции на атомно ниво. Използвайки Interlocked клас, тези операции са много по -бързи от използването на командата SyncLock.

Блокиране

По време на синхронизацията заключването се настройва върху обекти, а не върху нишки, така че при използване различенобекти за блокиране различенфрагменти от код в програмите понякога се случват доста нетривиални грешки. За съжаление, в много случаи синхронизацията на един обект е просто неприемлива, тъй като ще доведе до блокиране на нишки твърде често.

Помислете за ситуацията блокиране(задънена улица) в най -простата си форма. Представете си двама програмисти на масата за вечеря. За съжаление те имат само един нож и една вилица за двама. Ако приемем, че имате нужда от нож и вилица за ядене, са възможни две ситуации:

  • Един програмист успява да вземе нож и вилица и започва да яде. Когато е пълен, той оставя вечерята настрана и след това друг програмист може да ги вземе.
  • Един програмист взема ножа, а другият - вилицата. Никой не може да започне да яде, освен ако другият не се откаже от уреда си.

В многонишкова програма тази ситуация се нарича взаимно блокиране.Двата метода са синхронизирани на различни обекти. Тема А улавя обект 1 и влиза в програмната част, защитена от този обект. За съжаление, за да работи, той се нуждае от достъп до код, защитен от друга Sync Lock с различен обект за синхронизиране. Но преди да има време да въведе фрагмент, който е синхронизиран от друг обект, поток В влиза в него и улавя този обект. Сега нишката А не може да влезе във втория фрагмент, нишката В не може да влезе в първия фрагмент и двете нишки са обречени да чакат за неопределено време. Никоя нишка не може да продължи да работи, тъй като необходимия обект никога няма да бъде освободен.

Диагностицирането на задънени улици се усложнява от факта, че те могат да възникнат в сравнително редки случаи. Всичко зависи от реда, в който планировчикът им разпределя процесорното време. Възможно е в повечето случаи обектите за синхронизация да бъдат заснети в ред без блокиране.

Следва изпълнение на току -що описаната ситуация на задънена улица. След кратко обсъждане на най -фундаменталните моменти ще покажем как да идентифицираме ситуация на задънена улица в прозореца на нишката:

1 Опция Строго включено

2 Импортира система

3 Модул Модул

4 Sub Main ()

5 Dim Tom като нов програмист ("Tom")

6 Dim Bob като нов програмист ("Bob")

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

8 Затъмнете aThread като нова нишка (aThreadStart)

9 aThread.Name = "Том"

10 Dim bThreadStart As New ThreadStarttAddressOf Bob.Eat)

11 Затъмнете bThread като нова нишка (bThreadStart)

12 bThread.Name = "Боб"

13 aThread.Start ()

14 bThread.Start ()

15 End Sub

16 Краен модул

17 Вилица за публичен клас

18 Частна споделена mForkAvaiTable като Boolean = True

19 Private Shared mOwner As String = "Никой"

20 Частни собствености само за четене притежаватUtensil () As String

21 Вземи

22 Върнете mOwner

23 Край на Get

24 Крайна собственост

25 Public Sub GrabForktByVal a като програмист)

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

"се опитва да хване вилицата.")

27 Console.WriteLine (Me.OwnsUtensil & "има вилицата."). ...

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

29 Ако mForkAvailable Тогава

30 a.HasFork = Вярно

31 mOwner = a.MyName

32 mForkAvailable = Фалшиво

33 Console.WriteLine (a.MyName & "току -що получи вилката. Чакане")

34 Опитайте

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

Край Опитайте

35 Край Ако

36 Монитор. Изход (аз)

Прекратете SyncLock

37 End Sub

38 Краен клас

39 Публичен клас нож

40 Частен споделен mKnife Наличен като булев = Вярно

41 Private Shared mOwner As String = "Никой"

42 Частни собствености само за четене притежават Utensi1 () As String

43 Вземи

44 Върнете mOwner

45 Край на Get

46 Крайна собственост

47 Public Sub GrabKnifetByVal a като програмист)

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

"се опитва да хване ножа.")

49 Console.WriteLine (Me.OwnsUtensil & "има ножа.")

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

51 Ако mKnifeAvailable Тогава

52 mKnifeAvailable = невярно

53 a.HasKnife = Вярно

54 mOwner = a.MyName

55 Console.WriteLine (a.MyName & "току -що получи ножа. Чакане")

56 Опитайте

Thread.Sleep (100)

Хванете e като изключение

Console.WriteLine (e.StackTrace)

Край Опитайте

57 Край Ако

58 Монитор. Изход (аз)

59 End Sub

60 Краен клас

61 Програмист от публичен клас

62 Частно mName As String

63 Частен споделен mFork As Fork

64 Частен споделен mKnife As Knife

65 Частен mHasKnife Като булев

66 Частно mHasFork като булево

67 споделен под нов ()

68 mFork = Нова вилица ()

69 mKnife = Нож ()

70 End Sub

71 Public Sub New (ByVal theName As String)

72 mName = име

73 End Sub

74 Публично свойство само за четене MyName () As String

75 Вземете

76 Връщане на mName

77 Край на Get

78 Крайна собственост

79 Публична собственост HasKnife () Като булева

80 Вземи

81 Върнете mHasKnife

82 Край на Get

83 Set (ByVal Value As Boolean)

84 mHasKnife = Стойност

85 Краен комплект

86 Крайна собственост

87 Публична собственост HasFork () Като булева

88 Вземи

89 Върнете mHasFork

90 Край Get

91 Set (ByVal Value As Boolean)

92 mHasFork = Стойност

93 Краен комплект

94 Крайна собственост

95 Public Sub Eat ()

96 Направи до мен.Хас Нож и аз

97 Console.Writeline (Thread.CurrentThread.Name & "е в нишката.")

98 Ако Rnd ()< 0.5 Then

99 mFork.GrabFork (аз)

100 Иначе

101 mKnife.GrabKnife (аз)

102 Край Ако

103 Цикъл

104 MsgBox (Me.MyName & "мога да ям!")

105 mKnife = Нож ()

106 mFork = Нова вилица ()

107 End Sub

108 Краен клас

Основната процедура Main (редове 4-16) създава два екземпляра от класа Programmer и след това стартира две нишки за изпълнение на критичния метод Eat от класа Programmer (редове 95-108), описан по-долу. Процедурата Main задава имената на нишките и ги настройва; вероятно всичко, което се случва, е разбираемо и без коментар.

Кодът за класа Fork изглежда по-интересен (редове 17-38) (подобен клас Knife е дефиниран в редове 39-60). Редове 18 и 19 определят стойностите на общите полета, чрез които можете да разберете дали щепселът е наличен в момента и ако не, кой го използва. Свойството ReadOnly OwnUtensi1 (редове 20-24) е предназначено за най-простото предаване на информация. В центъра на класа Fork е методът GrabFork „grab the fork“, дефиниран в редове 25-27.

  1. Редове 26 и 27 просто отпечатват информация за отстраняване на грешки в конзолата. В основния код на метода (редове 28-36) достъпът до вилицата се синхронизира от обектколан Ме. Тъй като нашата програма използва само една вилка, Me sync гарантира, че две нишки не могат да я вземат едновременно. Командата Slee "p (в блока, започващ на ред 34) симулира закъснението между хващането на вилица / нож и началото на хранене. Обърнете внимание, че командата Sleep не отключва обекти и само ускорява блокирането!
    Най-интересният обаче е кодът на класа Programmer (редове 61-108). Редове 67-70 определят общ конструктор, за да се гарантира, че в програмата има само една вилица и нож. Кодът на имота (редове 74-94) е прост и не изисква коментар. Най -важното нещо се случва в метода Eat, който се изпълнява от две отделни нишки. Процесът продължава в цикъл, докато някакъв поток улови вилицата заедно с ножа. На редове 98-102 обектът произволно хваща вилицата / ножа, използвайки извикването Rnd, което е причината за блокирането. Случва се следното:
    Нишката, която изпълнява метода Eat на обекта Thoth, се извиква и влиза в цикъла. Той грабва ножа и влиза в състояние на изчакване.
  2. Нишката, изпълняваща метода на Bob's Eat, се извиква и влиза в цикъла. Не може да хване ножа, но хваща вилицата и преминава в състояние на изчакване.
  3. Нишката, която изпълнява метода Eat на обекта Thoth, се извиква и влиза в цикъла. Той се опитва да хване вилицата, но Боб вече е хванал вилицата; нишката преминава в състояние на изчакване.
  4. Нишката, изпълняваща метода на Bob's Eat, се извиква и влиза в цикъла. Той се опитва да хване ножа, но ножът вече е заловен от обекта Тот; нишката преминава в състояние на изчакване.

Всичко това продължава за неопределено време - изправени сме пред типична ситуация на задънена улица (опитайте да стартирате програмата и ще видите, че никой не може да се храни по този начин).
Можете също така да проверите дали в прозореца на нишките е настъпила блокировка. Стартирайте програмата и я прекъснете с клавишите Ctrl + Break. Включете променливата Me в изгледа и отворете прозореца потоци. Резултатът прилича на този, показан на фиг. 10.7. От фигурата можете да видите, че конецът на Боб е хванал нож, но няма вилица. Щракнете с десния бутон върху прозореца Threads на реда Tot и изберете командата Switch to Thread от контекстното меню. Изгледният прозорец показва, че потокът Thoth има вилица, но няма нож. Разбира се, това не е сто процентово доказателство, но такова поведение поне ви кара да подозирате, че нещо не е наред.
Ако опцията със синхронизация от един обект (както в програмата с повишаване на -температурата в къщата) не е възможна, за да предотвратите взаимни заключвания, можете да номерирате обектите за синхронизация и винаги да ги улавяте в постоянен ред. Нека продължим аналогията на програмиста за трапезария: ако нишката винаги първо взема ножа, а след това вилицата, няма да има проблеми с блокирането. Първият поток, който хваща ножа, ще може да се храни нормално. Преведено на езика на програмните потоци, това означава, че улавянето на обект 2 е възможно само ако обект 1 е заснет първо.

Ориз. 10.7. Анализ на блокировки в прозореца на нишката

Следователно, ако премахнем повикването към Rnd на ред 98 и го заменим с фрагмента

mFork.GrabFork (аз)

mKnife.GrabKnife (Аз)

задънена улица изчезва!

Сътрудничество по данни, докато се създават

В многонишковите приложения често има ситуация, при която нишките не само работят със споделени данни, но и чакат появата им (тоест нишка 1 трябва да създаде данни, преди нишката 2 да може да ги използва). Тъй като данните се споделят, достъпът до тях трябва да бъде синхронизиран. Необходимо е също така да се предвидят средства за уведомяване на чакащите нишки за появата на готови данни.

Тази ситуация обикновено се нарича проблема с доставчика / потребителя.Потокът се опитва да получи достъп до данни, които все още не съществуват, така че трябва да прехвърли контрола към друга нишка, която създава необходимите данни. Проблемът се решава със следния код:

  • Нишка 1 (потребител) се събужда, влиза в синхронизиран метод, търси данни, не ги намира и преминава в състояние на изчакване. Предварителнофизически, той трябва да премахне блокирането, за да не пречи на работата на захранващата нишка.
  • Нишка 2 (доставчик) влиза в синхронизиран метод, освободен от нишка 1, създаваданни за поток 1 и по някакъв начин уведомява поток 1 за наличието на данни. След това освобождава заключването, така че нишка 1 да може да обработва новите данни.

Не се опитвайте да решите този проблем, като постоянно извиквате нишка 1 и проверявате състоянието на променливата на състоянието, чиято стойност е> зададена от нишка 2. Това решение ще засегне сериозно работата на вашата програма, тъй като в повечето случаи нишка 1 ще да бъде извикан без причина; и нишка 2 ще чака толкова често, че ще изтече времето за създаване на данни.

Взаимоотношенията доставчик / потребител са много често срещани, така че за такива ситуации се създават специални примитиви в библиотеки с многонишково програмиране. В NET тези примитиви се наричат ​​Wait и Pulse-PulseAl 1 и са част от класа Monitor. Фигура 10.8 илюстрира ситуацията, която предстои да програмираме. Програмата организира три опашки от нишки: опашка за изчакване, опашка за блокиране и опашка за изпълнение. Планировчикът на нишки не разпределя времето на процесора за нишки, които са в опашката за изчакване. За да бъде разпределено време на нишка, тя трябва да се премести в опашката за изпълнение. В резултат на това работата на приложението е организирана много по -ефективно, отколкото при обичайното проучване на условна променлива.

В псевдокода идиомът на потребителя на данни се формулира, както следва:

„Влизане в синхронизиран блок от следния тип

Докато няма данни

Отидете на опашката за чакане

Цикъл

Ако има данни, обработете ги.

Оставете синхронизиран блок

Веднага след изпълнението на командата Wait нишката се спира, заключването се освобождава и нишката влиза в чакащата опашка. Когато заключването се освободи, нишката в опашката за изпълнение може да се изпълнява. С течение на времето една или повече блокирани нишки ще създадат данните, необходими за работата на нишката, която е в опашката за изчакване. Тъй като валидирането на данните се извършва в цикъл, преходът към използване на данните (след цикъла) се извършва само когато има данни, готови за обработка.

В псевдокода идиомът на доставчика на данни изглежда така:

„Влизане в блок за синхронизиран изглед

Докато данните НЕ са необходими

Отидете на опашката за чакане

В противен случай произвеждайте данни

Когато данните са готови, обадете се на Pulse-PulseAll.

за преместване на една или повече нишки от опашката за блокиране в опашката за изпълнение. Оставете синхронизирания блок (и се върнете към опашката за изпълнение)

Да предположим, че нашата програма симулира семейство с един родител, който печели пари, и дете, което харчи тези пари. Когато парите свършатсе оказва, че детето трябва да изчака пристигането на нова сума. Софтуерната реализация на този модел изглежда така:

1 Опция Строго включено

2 Импортира система

3 Модул Модул

4 Sub Main ()

5 Затъмнете семейството като ново семейство ()

6 theFamily.StartltsLife ()

7 End Sub

8 Краен fjodule

9

10 Обществено класно семейство

11 Частни mMoney като цяло число

12 Частен mWeek As Integer = 1

13 Public Sub StartltsLife ()

14 Dim aThreadStart As New ThreadStarUAddressOf Me.Produce)

15 Dim bThreadStart As New ThreadStarUAddressOf Me.Consume)

16 Dim aThread As New Thread (aThreadStart)

17 Затъмнете bThread като нова нишка (bThreadStart)

18 aThread.Name = "Производство"

19 aThread.Start ()

20 bThread.Name = "Консумирай"

21 bТема. Старт ()

22 End Sub

23 Публична собственост TheWeek () Като цяло число

24 Вземете

25 Връщане на седмица

26 Край на Get

27 Set (ByVal стойност като цяло число)

28 седмици - Стойност

29 Краен комплект

30 Краен имот

31 Публична собственост OurMoney () Като цяло число

32 Вземете

33 Върнете mMoney

34 Край на Get

35 Set (ByVal стойност като цяло число)

36 mПари = Стойност

37 Краен комплект

38 Крайна собственост

39 Публичен субпродукт ()

40 Thread.Sleep (500)

41 Направете

42 Монитор. Въведете (аз)

43 Направете докато аз.Нашите пари> 0

44 Монитор. Изчакайте (аз)

45 Цикъл

46 Аз. Нашите пари = 1000

47 Монитор.PulseAll (аз)

48 Монитор. Изход (аз)

49 Цикъл

50 End Sub

51 Публична подконсумация ()

52 MsgBox ("Аз съм в нишка за консумация")

53 Направете

54 Монитор. Въведете (аз)

55 Do While Me.OurMoney = 0

56 Монитор. Изчакайте (аз)

57 Цикъл

58 Console.WriteLine ("Скъпи родител, току -що изхарчих всичките ти" & _

пари през седмицата "& TheWeek)

59 Седмицата + = 1

60 Ако TheWeek = 21 * 52 Тогава System.Environment.Exit (0)

61 Аз.Нашите пари = 0

62 Монитор.PulseAll (Аз)

63 Монитор. Изход (аз)

64 цикъл

65 End Sub

66 Краен клас

Методът StartltsLife (редове 13-22) се подготвя за стартиране на потоците Производство и Консумиране. Най-важното нещо се случва в потоците Produce (редове 39-50) и Consume (редове 51-65). Процедурата за подпроизводство проверява наличността на пари и ако има пари, тя отива в опашката за изчакване. В противен случай родителят генерира пари (ред 46) и уведомява обектите в чакащата опашка за промяна в ситуацията. Обърнете внимание, че повикването към Pulse-Pulse All влиза в сила само когато заключването се освободи с командата Monitor.Exit. Обратно, процедурата Sub Subume проверява наличието на пари и ако няма пари, уведомява бъдещия родител за това. Ред 60 просто прекратява програмата след 21 условни години; извикваща система. Environment.Exit (0) е .NET аналог на командата End (командата End също се поддържа, но за разлика от System. Environment. Exit, тя не връща изходен код на операционната система).

Нишките, поставени в чакащата опашка, трябва да бъдат освободени от други части на вашата програма. Поради тази причина ние предпочитаме да използваме PulseAll вместо Pulse. Тъй като не е известно предварително коя нишка ще бъде активирана при извикване на Pulse 1, със сравнително малък брой нишки в опашката, можете също така да извикате PulseAll.

Многопоточност в графични програми

Нашето обсъждане на многопоточност в приложения с графичен потребителски интерфейс започва с пример, който обяснява за какво служи многонишковото в приложения с графичен интерфейс. Създайте формуляр с два бутона Start (btnStart) и Cancel (btnCancel), както е показано на фиг. 10.9. Щракването върху бутона Старт генерира клас, който съдържа произволен низ от 10 милиона знака и метод за преброяване на появата на буквата "E" в този дълъг низ. Обърнете внимание на използването на класа StringBuilder за по -ефективно създаване на дълги низове.

Етап 1

Тема 1 забелязва, че няма данни за нея. Той извиква Wait, освобождава заключването и отива в опашката за чакане.



Стъпка 2

Когато ключалката се освободи, нишка 2 или нишка 3 напуска блоковата опашка и влиза в синхронизиран блок, придобивайки заключването

Стъпка 3

Да предположим, че нишка 3 влиза в синхронизиран блок, създава данни и извиква Pulse-Pulse All.

Веднага след като излезе от блока и освободи заключването, нишка 1 се премества в опашката за изпълнение. Ако нишка 3 извика Pluse, само един влиза в опашката за изпълнениенишка, когато се извика Pluse All, всички нишки отиват в опашката за изпълнение.



Ориз. 10.8. Проблем с доставчика / потребителя

Ориз. 10.9. Многопоточност в просто GUI приложение

Импортира System.Text

Public Class RandomCharacters

Частен m_Data като StringBuilder

Частна mjength, m_count As Integer

Public Sub New (ByVal n As Integer)

m_Length = n -1

m_Data = Нов StringBuilder (m_length) MakeString ()

End Sub

Частен под MakeString ()

Dim i As Integer

Затъмнете myRnd като нов случаен ()

За i = 0 До m_length

"Генерирайте произволно число между 65 и 90,

"конвертирайте го в главни букви

"и прикачете към обекта StringBuilder

m_Data.Append (Chr (myRnd.Next (65.90)))

Следващия

End Sub

Public Sub StartCount ()

GetEes ()

End Sub

Частен под GetEes ()

Dim i As Integer

За i = 0 До m_length

Ако m_Data.Chars (i) = CChar ("E") Тогава

m_count + = 1

Край, ако следва

m_CountDone = Вярно

End Sub

Публично Само за четене

Свойство GetCount () As Integer Get

Ако не (m_CountDone) Тогава

Върнете m_count

Край Ако

Край Вземете крайно свойство

Публично Само за четене

Свойство IsDone () като Boolean Get

Връщане

m_CountDone

Край Get

Крайна собственост

Краен клас

Има много прост код, свързан с двата бутона на формуляра. Процедурата btn-Start_Click създава горния клас RandomCharacters, който капсулира низ с 10 милиона знака:

Частен под btnStart_Click (подател от ByVal като System.Object.

ByVal e As System.EventArgs) Обработва btnSTart.Click

Dim RC като нови случайни символи (10000000)

RC.StartCount ()

MsgBox ("Броят на es е" & RC.GetCount)

End Sub

Бутонът Отказ показва поле със съобщение:

Частен под btnCancel_Click (подател от ByVal като System.Object._

ByVal e As System.EventArgs) Обработва btnCancel.Click

MsgBox ("Прекъсване на броя!")

End Sub

Когато програмата се стартира и бутонът Старт е натиснат, се оказва, че бутонът Отказ не реагира на въвеждане от потребителя, тъй като непрекъснатият цикъл не позволява на бутона да обработва полученото събитие. Това е недопустимо в съвременните програми!

Има две възможни решения. Първата опция, добре позната от предишните версии на VB, се освобождава от многопоточност: повикването DoEvents е включено в цикъла. В NET тази команда изглежда така:

Application.DoEvents ()

В нашия пример това определено не е желателно - който иска да забави програма с десет милиона обаждания на DoEvents! Ако вместо това разпределите цикъла в отделна нишка, операционната система ще превключи между нишки и бутонът Отказ ще остане функционален. Реализацията с отделна нишка е показана по -долу. За да покажем ясно, че бутонът Отказ работи, когато кликнем върху него, просто прекратяваме програмата.

Следваща стъпка: Показване на бутона за броене

Да речем, че сте решили да проявите творческото си въображение и да придадете на формата вида, показан на фиг. 10.9. Моля, обърнете внимание: бутонът „Показване на броя“ все още не е наличен.

Ориз. 10.10. Заключена форма на бутон

Очаква се отделна нишка да извърши броенето и да отключи недостъпния бутон. Това разбира се може да се направи; освен това такава задача възниква доста често. За съжаление няма да можете да действате по най -очевидния начин - свържете вторичната нишка към нишката на GUI, като запазите връзка към бутона ShowCount в конструктора или дори използвайте стандартен делегат. С други думи, никогане използвайте опцията по -долу (основна погрешноредовете са удебелени).

Public Class RandomCharacters

Частен m_0ata като StringBuilder

Частен m_CountDone Като булев

Частна mjength. m_count като цяло число

Частен m_Button като Windows.Forms.Button

Public Sub New (ByVa1 n като цяло число, _

ByVal b Като Windows.Forms.Button)

m_length = n - 1

m_Data = Нов StringBuilder (mJength)

m_Button = b MakeString ()

End Sub

Частен под MakeString ()

Dim I като цяло число

Затъмнете myRnd като нов случаен ()

За I = 0 до m_length

m_Data.Append (Chr (myRnd.Next (65.90)))

Следващия

End Sub

Public Sub StartCount ()

GetEes ()

End Sub

Частен под GetEes ()

Dim I като цяло число

За I = 0 До mjength

Ако m_Data.Chars (I) = CChar ("E") Тогава

m_count + = 1

Край, ако следва

m_CountDone = Вярно

m_Button.Enabled = Вярно

End Sub

Публично Само за четене

Свойство GetCount () Като цяло число

Вземи

Ако не (m_CountDone) Тогава

Изхвърлете ново изключение ("Броя все още не е направено") иначе

Върнете m_count

Край Ако

Край Get

Крайна собственост

Публична собственост само за четене IsDone () Като булева

Вземи

Върнете m_CountDone

Край Get

Крайна собственост

Краен клас

Вероятно този код ще работи в някои случаи. Въпреки това:

  • Взаимодействието на вторичната нишка с нишката, създаваща GUI, не може да бъде организирано очевидноозначава.
  • Никогане променяйте елементи в графични програми от други програмни потоци. Всички промени трябва да се извършват само в нишката, създала графичния интерфейс.

Ако нарушите тези правила, ние ние гарантирамече фините, фини грешки ще се появят във вашите многопоточни графични програми.

Той също така няма да успее да организира взаимодействието на обекти с помощта на събития. Работникът с 06 събития работи в същата нишка, която се нарича RaiseEvent, така че събитията няма да ви помогнат.

Все пак здравият разум диктува, че графичните приложения трябва да имат средства за промяна на елементи от друга нишка. В NET Framework има безопасен за нишки начин за извикване на методи на GUI приложения от друга нишка. За тази цел се използва специален тип делегат за извикване на методи от пространството на имената System.Windows. Форми. Следният фрагмент показва нова версия на метода GetEes (променени редове с удебелен шрифт):

Частен под GetEes ()

Dim I като цяло число

За I = 0 до m_length

Ако m_Data.Chars (I) = CChar ("E") Тогава

m_count + = 1

Край, ако следва

m_CountDone = Истински опит

Dim mylnvoker As New Methodlnvoker (AddressOf UpDateButton)

myInvoker.Invoke () Catch e As ThreadlnterruptException

„Неуспех

Край Опитайте

End Sub

Публичен под UpDateButton ()

m_Button.Enabled = Вярно

End Sub

Извикванията между бутоните между нишките се извършват не директно, а чрез метод Invoker. .NET Framework гарантира, че тази опция е безопасна за нишки.

Защо има толкова много проблеми с многонишкото програмиране?

Сега, когато имате известно разбиране за многопоточността и потенциалните проблеми, свързани с нея, решихме, че би било подходящо да отговорим на въпроса в заглавието на този подраздел в края на тази глава.

Една от причините е, че многопоточността е нелинеен процес и сме свикнали с модел на линейно програмиране. Отначало е трудно да се свикне със самата идея, че изпълнението на програмата може да бъде прекъснато на случаен принцип и контролът ще бъде прехвърлен на друг код.

Има обаче и друга, по -фундаментална причина: тези дни програмистите твърде рядко програмират в асемблер или поне гледат разглобения изход на компилатора. В противен случай би било много по-лесно за тях да свикнат с идеята, че десетки инструкции за сглобяване могат да съответстват на една команда на език от високо ниво (като VB .NET). Нишката може да бъде прекъсната след някоя от тези инструкции и следователно в средата на команда от високо ниво.

Но това не е всичко: съвременните компилатори оптимизират работата на програмата, а компютърният хардуер може да попречи на управлението на паметта. В резултат на това компилаторът или хардуерът могат да променят реда на командите, посочени в изходния код на програмата, без ваше знание [ Много компилатори оптимизират циклично копиране на масиви, като например от i = 0 до n: b (i) = a (i): ncxt. Компилаторът (или дори специализиран мениджър на памет) може просто да създаде масив и след това да го запълни с една операция на копиране, вместо да копира отделни елементи многократно!].

Надяваме се, че тези обяснения ще ви помогнат да разберете по -добре защо многонишковото програмиране причинява толкова много проблеми - или поне по -малко изненада от странното поведение на вашите многонишкови програми!

Пример за изграждане на просто приложение с много нишки.

Роден от причината за много въпроси относно изграждането на многонишкови приложения в Delphi.

Целта на този пример е да покаже как правилно да се изгради многопоточно приложение, с премахването на дългосрочна работа в отделна нишка. И как в такова приложение да се осигури взаимодействието на основната нишка с работника за прехвърляне на данни от формата (визуални компоненти) към потока и обратно.

Примерът не претендира за пълен, той само демонстрира най -простите начини за взаимодействие между нишките. Позволяване на потребителя да "бързо заслепи" (кой би знаел колко го мразя) на правилно работещо многонишково приложение.
Всичко в него е коментирано подробно (според мен), но ако имате въпроси, питайте.
Но още веднъж ви предупреждавам: Потоците не са лесни... Ако нямате представа как всичко работи, тогава съществува огромна опасност често всичко да работи добре за вас и понякога програмата ще се държи повече от странно. Поведението на неправилно написана многонишка програма е силно зависима от голям брой фактори, които понякога не могат да бъдат възпроизведени по време на отстраняване на грешки.

Така че пример. За удобство поставих и кода, и прикачих архива с кода на модула и формуляра

единица ExThreadForm;

използва
Windows, съобщения, SysUtils, варианти, класове, графики, контроли, формуляри,
Диалози, StdCtrls;

// константи, използвани при прехвърляне на данни от поток във формуляр с помощта
// изпращане на прозоречни съобщения
const
WM_USER_SendMessageMetod = WM_USER + 10;
WM_USER_PostMessageMetod = WM_USER + 11;

Тип
// описание на класа на нишката, потомък на tThread
tMyThread = клас (tThread)
частни
SyncDataN: Цело число;
SyncDataS: Низ;
процедура SyncMetod1;
защитени
процедура Изпълнение; отменя;
обществен
Param1: Низ;
Param2: Цело число;
Param3: булев;
Спряно: булев;
LastRandom: Integer;
IterationNo: Integer;
ResultList: tStringList;

Конструктор Създаване (aParam1: Низ);
деструктор Унищожи; отменя;
край;

// описание на класа на формуляра с помощта на потока
TForm1 = клас (TForm)
Label1: TLabel;
Бележка1: TMemo;
btnStart: TButton;
btnStop: TButton;
Edit1: TEdit;
Edit2: TEdit;
CheckBox1: TCheckBox;
Label2: TLabel;
Label3: TLabel;
Label4: TLabel;
процедура btnStartClick (Подател: TObject);
процедура btnStopClick (Подател: TObject);
частни
(Частни декларации)
MyThread: tMyThread;
процедура EventMyThreadOnTerminate (Подател: tObject);
процедура EventOnSendMessageMetod (var Msg: TMessage); съобщение WM_USER_SendMessageMetod;
процедура EventOnPostMessageMetod (var Msg: TMessage); съобщение WM_USER_PostMessageMetod;

Обществен
(Публични декларации)
край;

вар
Форма1: TForm1;

{
Спряно - демонстрира прехвърлянето на данни от формуляр в поток.
Допълнителна синхронизация не се изисква, тъй като е проста
тип с една дума и е написан само от една нишка.
}

процедура TForm1.btnStartClick (Подател: TObject);
започнете
Randomize (); // осигуряване на случайност в последователността чрез Random () - няма нищо общо с потока

// Създаване на екземпляр на поточния обект, като му се предаде входен параметър
{
ВНИМАНИЕ!
Конструкторът на потока е написан по такъв начин, че потокът е създаден
спряно, защото позволява:
1. Контролирайте момента на стартирането му. Това почти винаги е по -удобно, защото
ви позволява да настроите поток дори преди стартиране, предайте го на вход
параметри и др.
2. Защото тогава връзката към създадения обект ще бъде записана в полето за формуляр
след саморазрушаване на нишката (виж по-долу), която когато нишката работи
може да възникне по всяко време, тази връзка ще стане невалидна.
}
MyThread: = tMyThread.Create (Form1.Edit1.Text);

// Въпреки това, тъй като нишката е създадена спряна, тогава при всякакви грешки
// по време на инициализацията му (преди да започнем), трябва да го унищожим сами
// за това, което използваме try / except block
опитвам

// Присвояване на манипулатор за прекратяване на нишка, в който ще получим
// резултатите от работата на потока и "презаписване" на връзката към него
MyThread.OnTerminate: = EventMyThreadOnTerminate;

// Тъй като резултатите ще бъдат събрани в OnTerminate, т.е. преди самоунищожението
// потокът тогава ще премахнем притесненията да го унищожим
MyThread.FreeOnTerminate: = Вярно;

// Пример за предаване на входни параметри през полетата на поточния обект, в точката
// създаване на екземпляр, когато още не се изпълнява.
// Лично аз предпочитам да правя това чрез параметрите на отмененото
// конструктор (tMyThread.Create)
MyThread.Param2: = StrToInt (Form1.Edit2.Text);

MyThread.Stopped: = False; // също един вид параметър, но променящ се
// време на работа на нишката
с изключение
// тъй като нишката все още не е стартирала и няма да може да се самоунищожи, ще я унищожим "ръчно"
FreeAndNil (MyThread);
// и след това нека изключението да се обработва както обикновено
повишаване;
край;

// Тъй като обектът на нишката е успешно създаден и конфигуриран, е време да го стартираме
MyThread.Resume;

ShowMessage ("Потокът стартира");
край;

процедура TForm1.btnStopClick (Подател: TObject);
започнете
// Ако екземплярът на нишката все още съществува, помолете го да спре
// И точно "попитай". По принцип също можем да „принудим“, но ще стане
// изключително спешен вариант, изискващ ясно разбиране на всичко това
// поточна кухня. Следователно тук не се разглежда.
ако е присвоен (MyThread), тогава
MyThread.Stopped: = Вярно
иначе
ShowMessage ("Нишката не работи!");
край;

процедура TForm1.EventOnSendMessageMetod (var Msg: TMessage);
започнете
// метод за обработка на синхронно съобщение
// в WParam адресът на обекта tMyThread, в LParam текущата стойност на LastRandom на нишката
с tMyThread (Msg.WParam) започнете
Form1.Label3.Caption: = Формат ("% d% d% d",);
край;
край;

процедура TForm1.EventOnPostMessageMetod (var Msg: TMessage);
започнете
// метод за обработка на асинхронно съобщение
// в WParam текущата стойност на IterationNo, в LParam текущата стойност на потока LastRandom
Form1.Label4.Caption: = Формат ("% d% d",);
край;

процедура TForm1.EventMyThreadOnTerminate (Подател: tObject);
започнете
// ВАЖНО!
// Методът за обработка на събитието OnTerminate винаги се извиква в контекста на main
// нишка - това е гарантирано от реализацията на tThread. Следователно, в него можете свободно
// използваме всякакви свойства и методи на всякакви обекти

// За всеки случай се уверете, че екземплярът на обекта все още съществува
ако не е присвоен (MyThread), след това излезте; // ако го няма, няма какво да се прави

// получаваме резултатите от работата на нишката на екземпляра на обекта на нишката
Form1.Memo1.Lines.Add (Format ("Потокът завърши с резултат% d",));
Form1.Memo1.Lines.AddStrings ((Изпращач като tMyThread) .ResultList);

// Унищожаваме препратката към екземпляра на поточния обект.
// Тъй като нашата нишка се саморазрушава (FreeOnTerminate: = True)
// след като манипулаторът OnTerminate завърши, екземплярът на поточния обект ще бъде
// унищожен (безплатен) и всички препратки към него ще станат невалидни.
// За да не попаднете случайно в такава връзка, презапишете MyThread
// Ще отбележа още веднъж - няма да унищожим обекта, а само да презапишем връзката. Предмет
// унищожава себе си!
MyThread: = Нищо;
край;

конструктор tMyThread.Create (aParam1: String);
започнете
// Създаване на екземпляр от суспендирания поток (вижте коментара при създаване на екземпляр)
наследен Create (True);

// Създаване на вътрешни обекти (ако е необходимо)
ResultList: = tStringList.Create;

// Получаване на първоначални данни.

// Копирайте входните данни, преминали през параметъра
Param1: = aParam1;

// Пример за получаване на входни данни от VCL компоненти в конструктора на поток обект
// Това е приемливо в този случай, тъй като конструкторът се извиква в контекста
// основна нишка. Следователно, компонентите на VCL могат да бъдат достъпни тук.
// Но това не ми харесва, защото мисля, че е лошо, когато нишката знае нещо
// за някаква форма там. Но какво не можете да направите за демонстрация.
Param3: = Form1.CheckBox1.Checked;
край;

деструктор tMyThread.Destroy;
започнете
// унищожаване на вътрешни обекти
FreeAndNil (ResultList);
// унищожава базата tThread
наследствено;
край;

процедура tMyThread.Execute;
вар
t: Кардинал;
s: Низ;
започнете
IterationNo: = 0; // брояч на резултатите (номер на цикъла)

// В моя пример тялото на нишката е цикъл, който завършва
// или чрез външно „искане“ за прекратяване, преминало през променливия параметър Стопирано,
// или просто като направите 5 бримки
// За мен е по -приятно да пиша това чрез "вечен" цикъл.

Докато True започва

Inc (IterationNo); // номер на следващия цикъл

LastRandom: = Random (1000); // номер на ключ - за демонстриране на прехвърлянето на параметри от потока към формата

T: = Случайно (5) +1; // време, за което ще заспим, ако не сме завършили

// Тъпа работа (в зависимост от входния параметър)
ако не Param3 тогава
Inc (Param2)
иначе
Дек (Param2);

// Формира междинен резултат
s: = Формат ("% s% 5d% s% d% d",
);

// Добавяне на междинен резултат към списъка с резултати
ResultList.Add (s);

//// Примери за предаване на междинен резултат във форма

//// Преминаване през синхронизиран метод - класическият начин
//// Недостатъци:
//// - методът, който се синхронизира, обикновено е метод от класа на потока (за достъп
//// към полетата на обекта на потока), но за достъп до полетата на формуляра, той трябва
//// "знам" за него и неговите полета (обекти), което обикновено не е много добре
//// гледна точка на организацията на програмата.
//// - текущата нишка ще бъде спряна, докато изпълнението приключи
//// синхронизиран метод.

//// Предимства:
//// - стандартен и универсален
//// - в синхронизиран метод можете да използвате
//// всички полета на поточния обект.
// първо, ако е необходимо, трябва да запишете прехвърлените данни в
// специални полета на обекта обект.
SyncDataN: = IterationNo;
SyncDataS: = "Sync" + s;
// и след това осигурете извикване на синхронизиран метод
Синхронизиране (SyncMetod1);

//// Изпращане чрез синхронно изпращане на съобщение (SendMessage)
//// в този случай данните могат да се предават както чрез параметрите на съобщението (LastRandom),
//// и през полетата на обекта, предавайки адреса на екземпляра в параметъра на съобщението
//// на поточния обект - Integer (Self).
//// Недостатъци:
//// - нишката трябва да познава дръжката на прозореца на формуляра
//// - както при Synchronize, текущата нишка ще бъде спряна до
//// завършване на обработката на съобщението от основната нишка
//// - изисква значително количество процесорно време за всяко обаждане
//// (за превключване на нишки) следователно много често повикване е нежелателно
//// Предимства:
//// - както при Synchronize, когато обработвате съобщение, можете да използвате
//// всички полета на поточния обект (ако, разбира се, неговият адрес е предаден)


//// стартиране на темата.
SendMessage (Form1.Handle, WM_USER_SendMessageMetod, Integer (Self), LastRandom);

//// Прехвърляне чрез асинхронно изпращане на съобщение (PostMessage)
//// Тъй като в този случай, докато съобщението бъде получено от основната нишка,
//// изпращащият поток може вече да е приключил, прехвърляйки адреса на екземпляра
//// поток обект е невалиден!
//// Недостатъци:
//// - нишката трябва да познава дръжката на прозореца на формуляра;
//// - поради асинхронност прехвърлянето на данни е възможно само чрез параметри
//// съобщения, което значително усложнява прехвърлянето на данни, които имат размер
//// повече от две машинни думи. Удобно е да се използва за преминаване на Integer и т.н.
//// Предимства:
//// - за разлика от предишните методи, текущата нишка НЯМА
//// е поставен на пауза и незабавно ще възобнови изпълнението
//// - за разлика от синхронизирано обаждане, манипулатор на съобщения
//// е метод на формуляр, който трябва да има познания за поточния обект,
//// или изобщо не знаят нищо за потока, ако данните се предават само
//// чрез параметри на съобщението. Тоест нишката може да не знае нищо за формата.
//// като цяло - само нейната дръжка, която може да бъде предадена като параметър преди това
//// стартиране на темата.
PostMessage (Form1.Handle, WM_USER_PostMessageMetod, IterationNo, LastRandom);

//// Проверете за възможно завършване

// Проверете за завършване по параметър
ако е спряно, тогава Break;

// Понякога проверявайте за завършеност
ако IterationNo> = 10, тогава Break;

Сън (t * 1000); // Заспиване за t секунди
край;
край;

процедура tMyThread.SyncMetod1;
започнете
// този метод се извиква чрез метода Synchronize.
// Тоест, въпреки факта, че това е метод на нишката tMyThread,
// работи в контекста на основната нишка на приложението.
// Следователно, той може всичко, добре или почти всичко :)
// Но запомнете, не си струва да се "бъркате" тук дълго време

// Предаваните параметри можем да извлечем от специалните полета, където ги имаме
// запазено преди обаждане.
Form1.Label1.Caption: = SyncDataS;

// или от други полета на поточния обект, например, отразяващи текущото му състояние
Form1.Label2.Caption: = Формат ("% d% d",);
край;

Като цяло примерът беше предшестван от моите разсъждения по темата ...

Първо:
НАЙ -ВАЖНОТО правило на многонишковото програмиране в Delphi е:
В контекста на не-основна нишка, нямате достъп до свойствата и методите на формулярите и наистина до всички компоненти, които "растат" от tWinControl.

Това означава (донякъде опростено), че нито в метода Execute, наследен от TThread, нито в други методи / процедури / функции, извикани от Execute, забранено едиректен достъп до всички свойства и методи на визуални компоненти.

Как да го направите правилно.
Няма унифицирани рецепти. По -точно, има толкова много и различни опции, че в зависимост от конкретния случай, трябва да изберете. Следователно те се позовават на статията. След като го прочете и разбере, програмистът ще може да разбере и как най -добре да го направи в конкретен случай.

Накратко на пръсти:

Най-често многонишкото приложение става или когато е необходимо да се извърши някаква дългосрочна работа, или когато е възможно едновременно да се направят няколко неща, които не натоварват силно процесора.

В първия случай изпълнението на работа вътре в основната нишка води до „забавяне“ на потребителския интерфейс - докато работата се извършва, цикълът на съобщението не се изпълнява. В резултат на това програмата не реагира на действия на потребителя и формулярът не се изчертава например, след като потребителят го премести.

Във втория случай, когато работата включва активен обмен с външния свят, тогава по време на принудителното „престой“. Докато чакате за получаване / изпращане на данни, можете да правите нещо друго паралелно, например отново да изпращате / получавате данни.

Има и други случаи, но по -рядко. Това обаче няма значение. Сега не става въпрос за това.

Сега как е написано всичко. Естествено се разглежда определен най -чест случай, донякъде обобщен. Така.

Работата, извършена в отделна нишка, в общия случай има четири обекта (не знам как да го нарека по -точно):
1. Първоначални данни
2. Всъщност самата работа (може да зависи от първоначалните данни)
3. Междинни данни (например информация за текущото състояние на изпълнение на работата)
4. Изходни данни (резултат)

Най -често визуалните компоненти се използват за четене и показване на повечето данни. Но, както бе споменато по -горе, нямате пряк достъп до визуалните компоненти от потока. Как да бъде?
Разработчиците на Delphi предлагат използването на метода Synchronize на класа TThread. Тук няма да описвам как да го използвам - има гореспоменатата статия за това. Нека само да кажа, че приложението му, дори и правилното, не винаги е оправдано. Има два проблема:

Първо, тялото на метод, наречен чрез синхронизиране, винаги се изпълнява в контекста на основната нишка и следователно, докато се изпълнява, отново цикълът на съобщението на прозореца не се изпълнява. Следователно, той трябва да бъде изпълнен бързо, в противен случай ще получим същите проблеми, както при еднопоточно изпълнение. В идеалния случай метод, извикан чрез Synchronize, обикновено трябва да се използва само за достъп до свойства и методи на визуални обекти.

Второ, изпълнението на метод чрез Synchronize е "скъпо" удоволствие поради необходимостта от два превключвателя между нишки.

Нещо повече, и двата проблема са взаимосвързани и предизвикват противоречие: от една страна, за да се реши първият, е необходимо да се "смилат" методите, извикани чрез синхронизиране, а от друга, те често трябва да бъдат извиквани, губейки ценни ресурси на процесора.

Следователно, както винаги, е необходимо да се подходи разумно и за различни случаи да се използват различни начини на взаимодействие на потока с външния свят:

Първоначални данни
Всички данни, които се прехвърлят към потока и не се променят по време на работата му, трябва да бъдат прехвърлени още преди да започне, т.е. при създаване на поток. За да ги използвате в тялото на нишка, трябва да направите локално копие от тях (обикновено в полетата на потомка на TThread).
Ако има първоначални данни, които могат да се променят, докато нишката работи, тогава тези данни трябва да бъдат достъпни или чрез синхронизирани методи (методи, извикани чрез Synchronize), или чрез полетата на обекта на нишката (потомък на TThread). Последното изисква известна предпазливост.

Междинни и изходни данни
Тук отново има няколко начина (в моя ред):
- Метод за асинхронно изпращане на съобщения до главния прозорец на приложението.
Обикновено се използва за изпращане на съобщения за хода на процеса до главния прозорец на приложението, с прехвърляне на малко количество данни (например процент на завършеност)
- Метод за синхронно изпращане на съобщения до главния прозорец на приложението.
Обикновено се използва за същите цели като асинхронното изпращане, но ви позволява да прехвърляте по -голямо количество данни, без да създавате отделно копие.
- Синхронизирани методи, ако е възможно, комбиниращи прехвърлянето на възможно най -много данни в един метод.
Може да се използва и за извличане на данни от формуляр.
- Чрез полетата на поточния обект, осигуряващ взаимно изключващ се достъп.
Повече подробности можете да намерите в статията.

Ех Не се получи за кратко