Журнал LinuxFormat - перейти на главную

LXF158:Erlang: Изу­чим мно­го­за­дач­ность

Материал из Linuxformat
Перейти к: навигация, поиск


Erlang - Опи­сы­ва­ет­ся сле­дую­щей фор­му­лой: функ­цио­наль­ный язык + про­цес­сы

Erlang: Изу­чим мно­го­за­дач­ность

Ан­д­рей Уша­ков ув­ле­чен взаи­мо­дей­ст­ви­ем ме­ж­ду про­цес­са­ми.

(thumbnail)
Наш эксперт Ан­д­рей Уша­ков ак­тив­но при­бли­жа­ет тот день, ко­гда функ­цио­наль­ные язы­ки ста­нут мейн­ст­ри­мом.

Со­вре­мен­ный мир раз­ра­бот­ки ПО слож­но пред­ста­вить без та­ко­го сред­ст­ва, как мно­го­за­дач­ность. Мно­го­за­дач­ность по­зво­ля­ет ре­шать мно­гие за­да­чи за при­ем­ле­мое вре­мя бла­го­да­ря то­му, что мно­гие час­ти од­ной за­да­чи вы­пол­ня­ют­ся од­но­вре­мен­но. Мно­го­за­дач­ность – это од­на из са­мых слож­ных об­лас­тей про­грам­ми­ро­вания. Язык Erlang в свое вре­мя был спе­ци­аль­но раз­ра­бо­тан для ре­шения боль­шин­ст­ва техниче­­ских про­блем, свя­зан­ных с мно­го­за­дач­но­стью. Эта стать­я открыва­ет боль­шой цикл, по­свя­щен­ный соз­данию мно­го­за­дач­ных и рас­пре­де­лен­ных при­ло­жений средствами язы­ка Erlang.

А начнем мы наш раз­го­вор с оп­ре­де­ления основ­ных тер­ми­нов, свя­зан­ных с те­мой на­ше­го раз­го­во­ра. Пер­вым де­лом да­дим оп­ре­де­ление тер­ми­ну «мно­го­за­дач­ность». Мно­го­за­дач­ность – это свой­ст­во язы­ка про­грам­ми­ро­вания, сре­ды вы­полнения и опе­ра­ци­он­ной сис­те­мы по обес­пе­чению од­но­вре­мен­но­го (или псев­до­од­нов­ре­мен­но­го) вы­полнения несколь­ких за­дач. Ес­ли язык про­грам­ми­ро­вания не яв­ля­ет­ся мно­го­за­дач­ным, то и бес­по­ко­ить­ся нам не о чем: все за­да­чи у нас вы­пол­ня­ют­ся по­сле­до­ва­тель­но. Ес­ли сре­да вы­полнения или опе­ра­ци­он­ная сис­те­ма не яв­ля­ют­ся мно­го­за­дач­ны­ми, то мак­си­мум, что мы смо­жем по­лу­чить – это ил­лю­зию мно­го­за­дач­но­сти. Эта ил­лю­зия за­клю­ча­ет­ся в том, что по­ток вы­полнения у нас в ре­аль­но­сти все­го один; сре­да вы­полнения или ОС пе­ре­клю­ча­ют его на вы­полнение разных фраг­мен­тов ко­да.

Ес­ли и язык про­грам­ми­ро­вания, и сре­да вы­полнения, и ОС под­дер­жи­ва­ют мно­го­за­дач­ность, то что­бы добиться од­но­вре­мен­ного вы­полнения несколь­ких за­дач, необ­хо­ди­мо вы­полнить два усло­вия. Во-пер­вых, ис­тин­ную мно­го­за­дач­ность мы мо­жем по­лу­чить, за­пуская несколь­ко за­дач на мно­го­про­цес­сор­ном или мно­го­ядер­ном ком­пь­ю­те­ре, или на несколь­ких ком­пь­ю­те­рах, со­единен­ных се­тью. Во-вто­рых, и это, по­жа­луй, са­мое важ­ное, для од­но­вре­мен­но­го вы­полнения несколь­ких за­дач в рам­ках од­ной про­грам­мы про­грам­ма из­на­чаль­но должна раз­ра­ба­ты­ваться как мно­го­за­дач­ная.

Мно­го­за­дач­ность бывает двух типов: осно­ван­ная на про­цес­сах и осно­ван­ная на по­то­ках. Го­во­ря о мно­го­за­дач­но­сти, осно­ван­ной на по­то­ках, мы имеем в виду, что у нас есть несколь­ко по­то­ков вы­полнения ко­да в рам­ках од­но­го про­цес­са. При та­кой кон­фи­гу­ра­ции несколь­ко за­дач од­но­вре­мен­но вы­пол­няют­ся в од­ном ад­рес­ном про­стран­ст­ве; это уп­ро­ща­ет взаи­мо­дей­ст­вие ме­ж­ду за­да­ча­ми, но при неправиль­ной реа­ли­за­ции данного взаи­мо­дей­ст­вия мо­жет при­вес­ти к несо­гла­со­ван­но­сти и пор­че дан­ных.

Мно­го­за­дач­но­сть, осно­ван­ная на про­цес­сах, подразумевает, что у нас есть несколь­ко про­цес­сов на од­ном или несколь­ких ком­пь­ю­те­рах. При та­кой кон­фи­гу­ра­ции у ка­ж­дой за­да­чи свое ад­рес­ное про­стран­ст­во, что пре­дот­вра­ща­ет мно­гие слу­чаи воз­мож­ной пор­чи со­стояния од­ной за­да­чи дру­гой за­да­чей, но услож­ня­ет взаи­мо­дей­ст­вие ме­ж­ду за­да­ча­ми. В дальней­шем мы бу­дем раз­ли­чать си­туа­ции, когда несколь­ко про­цес­сов вы­пол­ня­ют­ся на од­ном ком­пь­ю­те­ре и когда несколь­ко про­цес­сов вы­пол­ня­ют­ся на раз­ных ком­пь­ю­те­рах, со­единен­ных се­тью. Свя­за­но это с тем, что спо­со­бы взаи­мо­дей­ст­вия ме­ж­ду про­цес­са­ми за­ви­сят от то­го, на од­ном или на раз­ных ком­пь­ю­те­рах про­цес­сы вы­пол­ня­ют­ся.

Мно­го­за­дач­ность, осно­ван­ную на про­цес­сах, вы­пол­няю­щих­ся на раз­ных ком­пь­ю­те­рах, мы бу­дем рас­смат­ри­вать от­дель­но. Та­кой вариант много­за­дач­но­сти на­зы­ва­ют рас­пре­де­лен­ной сис­те­мой (а са­му та­кую мно­го­за­дач­ность – рас­пре­де­лен­ны­ми вы­чис­ления­ми). Глав­ное пре­иму­ще­ст­во рас­пре­де­лен­ных сис­тем пе­ред нерас­пре­де­лен­ны­ми в том, что их мощ­ность (ко­ли­че­­ст­во ком­пь­ю­те­ров, объ­е­динен­ных се­тью) мож­но на­ра­щи­вать бес­конеч­но. Ми­ну­сы по­доб­ных сис­тем в том, что их доста­точ­но слож­но соз­да­вать и под­дер­жи­вать их ра­бо­ту (вполне оче­вид­но, что чем боль­ше та­кая рас­пре­де­лен­ная сис­те­ма, тем это де­лать сложнее). Кро­ме то­го, да­ле­ко не вся­кую за­да­чу име­ет смысл ре­шать в рас­пре­де­лен­ной сис­те­ме; но это те­ма для от­дель­ной ста­тьи.

Вме­сто это­го да­вай­те рас­смот­рим класс за­дач, ре­шение ко­то­рых в рас­пре­де­лен­ной сис­те­ме да­ет су­ще­ст­вен­ный вы­иг­рыш по сравнению с ре­шением нерас­пре­де­лен­ным и од­но­за­дач­ным спо­со­бом. Это за­да­чи по по­ис­ку эле­мен­та из неко­то­ро­го мно­же­ст­ва ме­то­дом пол­но­го пе­ре­бо­ра (или ме­то­дом «гру­бой си­лы», англ. brute force): мы пе­ре­би­ра­ем все эле­мен­ты из мно­же­ст­ва, по­ка не най­дем удов­ле­тво­ряю­щий нас эле­мент.

Оче­вид­но, что ес­ли мно­же­ст­во эле­мен­тов упо­ря­до­че­но и су­ще­ст­ву­ет связь ме­ж­ду кри­те­ри­ем по­ис­ка (бли­зостью эле­мента к ис­ко­мо­му) и по­ряд­ком эле­мен­тов, то ме­тод по­ис­ка пол­ным пе­ре­бо­ром в та­кой си­туа­ции не оп­ти­ма­лен. Так­же вполне оче­вид­но, что ес­ли мы раз­де­лим мно­же­ст­во эле­мен­тов на N рав­ных под­мно­жеств и начнем по­иск в этих под­мно­же­ст­вах од­но­вре­мен­но, то среднее вре­мя по­ис­ка бу­дет в N раз мень­ше среднего вре­мени по­ис­ка во всем мно­же­ст­ве эле­мен­тов. По­это­му вполне ло­гич­но, что та­кой класс за­дач хо­ро­шо под­хо­дит для ре­шения на рас­пре­де­лен­ных сис­те­мах. К та­ко­му клас­су за­дач от­но­сит­ся, на­при­мер, за­да­ча под­бо­ра па­ро­ля по хэ­шу MD5 (хо­тя для этой за­да­чи су­ще­ст­ву­ет и аль­тер­на­тив­ный под­ход к ре­шению, с ис­поль­зо­ванием ра­дуж­ных таб­лиц).

Пой­дем даль­ше и оста­но­вим­ся на еще од­ном по­ня­тии, без ко­то­ро­го невоз­мож­но соз­да­вать бо­лее-менее слож­ные мно­го­за­дач­ные про­грам­мы: это взаи­мо­дей­ст­вие ме­ж­ду за­да­ча­ми (обыч­но на­зы­вае­мое взаи­мо­дей­ст­ви­ем ме­ж­ду про­цес­са­ми или IPC). Сред­ст­ва, при­ме­няе­мые для та­ко­го взаи­мо­дей­ст­вия, за­ви­сят от вида ис­поль­зу­емой мно­го­за­дач­ности (осно­ван­ной на по­то­ках, осно­ван­ной на про­цес­сах на од­ном ком­пь­ю­те­ре или осно­ван­ной на ис­поль­зо­вании рас­пре­де­лен­ных вы­чис­лений), а так­же от язы­ка про­грам­ми­ро­вания (ес­ли та­кие сред­ст­ва встрое­ны в язык), сре­ды вы­полнения и ОС. Вполне ло­гич­но, что сред­ст­ва, при­ме­няе­мые для взаи­мо­дей­ст­вия про­цес­сов в рас­пре­де­лен­ной сис­те­ме, мо­гут быть при­менены и для взаи­мо­дей­ст­вия про­цес­сов на од­ном ком­пь­ю­те­ре, и для взаи­мо­дей­ст­вия по­то­ков в од­ном про­цес­се. А сред­ст­ва, при­ме­няе­мые для взаи­мо­дей­ст­вия про­цес­сов на од­ном ком­пь­ю­те­ре, пригодны и для взаи­мо­дей­ст­вия по­то­ков внут­ри про­цес­са.

Возника­ет во­прос: зачем нам ис­поль­зо­вать бо­лее об­щие сред­ст­ва взаи­мо­дей­ст­вия в слу­чае, когда мож­но при­менить бо­лее под­хо­дя­щие для дан­но­го ти­па мно­го­за­дач­но­сти? Од­на из при­чин та­ко­го под­хо­да со­сто­ит в том, что, раз­ра­ба­ты­вая из­на­чаль­но взаи­мо­дей­ст­вие ме­ж­ду за­да­ча­ми та­ким спо­со­бом, мы по­лу­ча­ем воз­мож­ность ис­поль­зо­вать наш код для лю­бых ви­дов мно­го­за­дач­но­сти. Кро­ме то­го, при та­ком под­хо­де мы мо­жем из­бе­жать или миними­зи­ро­вать возник­но­вение од­но­вре­мен­но­го досту­па к дан­ным (точнее го­во­ря, нам на­до из­бе­гать од­но­вре­мен­но­го досту­па к дан­ным, ес­ли мы хо­тим из­ме­нять эти дан­ные) и, как след­ст­вие, воз­мож­но­го по­вре­ж­дения и несо­гла­со­ван­но­сти дан­ных.

Ти­пы псев­до­па­рал­лель­ной мно­го­за­дач­но­сти
  • Не­вы­тес­няю­щая мно­го­за­дач­ность Тип мно­го­за­дач­но­сти, при ко­то­ром опе­ра­ци­он­ная сис­те­ма мо­жет за­гру­зить бо­лее од­но­го при­ло­жения в па­мять, но вре­мя про­цес­сора пре­достав­ля­ет­ся толь­ко од­но­му из них – основ­но­му при­ло­жению. Ос­таль­ные при­ло­жения яв­ля­ют­ся фо­но­вы­ми. Для пре­достав­ления про­цес­сор­но­го вре­мени фо­но­во­му при­ло­жению его необ­хо­ди­мо ак­ти­ви­зи­ро­вать. По­доб­ная мно­го­за­дач­ность мо­жет быть реа­ли­зо­ва­на не толь­ко в ОС, но и с по­мо­щью про­грамм – пе­ре­клю­ча­те­лей за­дач.
  • Коо­пе­ра­тив­ная мно­го­за­дач­ность Тип мно­го­за­дач­но­сти, при ко­то­ром сле­дую­щая за­да­ча вы­пол­ня­ет­ся толь­ко по­сле то­го, как те­ку­щая за­да­ча яв­но провозгласит се­бя го­то­вой от­дать про­цес­сор­ное вре­мя дру­гим за­да­чам. Как ча­ст­ный слу­чай, та­кое объ­яв­ление под­ра­зу­ме­ва­ет­ся при по­пыт­ке за­хва­та уже за­ня­то­го объ­ек­та бло­ки­ров­ки, а так­же при ожи­дании по­сту­п­ления сле­дую­ще­го со­об­щения от под­сис­те­мы поль­зо­ва­тель­ско­го ин­тер­фей­са.
  • Вы­тес­няю­щая мно­го­за­дач­ность Вид мно­го­за­дач­но­сти, в ко­то­ром ОС са­ма пе­ре­да­ет управ­ление от од­ной вы­пол­няе­мой про­грам­мы дру­гой в слу­чае за­вер­шения опе­ра­ций вво­да-вы­во­да, возник­но­вения со­бы­тий в ап­па­ра­ту­ре ком­пь­ю­те­ра, ис­те­чения тай­ме­ров и кван­тов вре­мени или же по­сту­п­лений тех или иных сиг­на­лов от од­ной про­грам­мы к дру­гой. В этом ви­де мно­го­за­дач­но­сти про­цес­сор мо­жет быть пе­ре­клю­чен с ис­полнения од­ной про­грам­мы на ис­полнение дру­гой без вся­ко­го по­же­лания пер­вой про­грам­мы и бу­к­валь­но ме­ж­ду лю­бы­ми дву­мя ин­ст­рук­ция­ми в ее ко­де. Рас­пре­де­ление про­цес­сор­но­го вре­мени осу­ще­ст­в­ля­ет­ся планиров­щи­ком про­цес­сов. К то­му же ка­ж­дой за­да­че мо­жет быть на­зна­чен поль­зо­ва­те­лем или са­мой опе­ра­ци­он­ной сис­те­мой оп­ре­де­лен­ный при­ори­тет, что обес­пе­чи­ва­ет гиб­кое управ­ление рас­пре­де­лением про­цес­сор­но­го вре­мени ме­ж­ду за­да­ча­ми (на­при­мер, мож­но снизить при­ори­тет ре­сур­со­ем­кой про­грам­ме, снизив тем са­мым ско­рость ее ра­бо­ты, но по­вы­сив про­из­во­ди­тель­ность фо­но­вых про­цес­сов). При этом обес­пе­чи­ва­ется бо­лее бы­ст­рый от­клик на дей­ст­вия поль­зо­ва­те­ля.

Те­перь да­вай­те раз­бе­ремся, ка­кие есть сред­ст­ва для соз­дания мно­го­за­дач­ных про­грамм. В ка­че­­ст­ве при­ме­ра та­ко­го средства рас­смот­рим ОС Linux. Пер­вый во­прос, ко­то­рый вста­ет пе­ред на­ми – как соз­давать но­вые за­да­чи. Начнем с соз­дания по­то­ков. В Linux для ра­бо­ты с по­то­ка­ми у нас есть биб­лио­те­ка pthreads (ин­те­рес­но, что по­то­ки в Linux – это про­цес­сы, раз­де­ляю­щие ре­сур­сы с про­цес­сом, ко­то­рый их соз­дал); для соз­дания но­вых по­то­ков ис­поль­зу­ет­ся функ­ция pthread_create. Пе­рей­дем к соз­данию про­цес­сов. Для соз­дания про­цес­сов у нас есть сле­дую­щие биб­лио­теч­ные функ­ции: функ­ция fork для соз­дания но­во­го про­цес­са, се­мей­ст­во функ­ций exec для за­пуска в рам­ках про­цес­са дру­гой про­грам­мы, функ­ция system для вы­полнения команд. Сле­ду­ет ска­зать, что соз­да­вать но­вые про­цес­сы мы мо­жем толь­ко на локаль­ном ком­пь­ю­те­ре; воз­мож­но­сти соз­дать но­вый про­цесс (и за­пустить в нем ка­кую-ли­бо про­грам­му) у нас нет. По­это­му при по­строении рас­пре­де­лен­ной сис­те­мы необ­хо­ди­мо пре­ду­смот­реть ав­то­ма­ти­че­­ский старт (при стар­те сис­те­мы) неко­то­ро­го про­цес­са для взаи­мо­дей­ст­вия уз­лов этой рас­пре­де­лен­ной сис­те­мы.

При­шла по­ра по­смот­реть, ка­кие есть сред­ст­ва взаи­мо­дей­ст­вия ме­ж­ду за­да­ча­ми в ОС Linux. Начнем с мно­го­за­дач­но­сти, осно­ван­ной на по­то­ках; для взаи­мо­дей­ст­вия за­дач у нас есть мью­тек­сы, бло­ки­ров­ки чтения-за­пи­си, услов­ные пе­ре­мен­ные, воз­мож­ность ожи­дания за­вер­шения по­то­ка. Все эти сред­ст­ва осно­ва­ны на ис­поль­зо­вании бло­ки­ро­вок по­то­ка вы­полнения. Мно­же­ст­во средств для мно­го­за­дач­но­сти, осно­ван­ной на локаль­ных про­цес­сах, доста­точ­но раз­но­об­раз­но: име­но­ван­ные и неиме­но­ван­ные ка­на­лы, фай­лы, оче­ре­ди со­об­щений, се­ма­фо­ры, раз­де­ляе­мая па­мять, со­ке­ты до­ме­на Unix, воз­мож­ность ожи­дания за­вер­шения про­цес­са. Ес­ли для взаи­мо­дей­ст­вия про­цес­сов мы ис­поль­зу­ем раз­де­ляе­мую па­мять или фай­лы, то при неак­ку­рат­ной ра­бо­те с дан­ны­ми (на­при­мер, когда несколь­ко про­цес­сов од­но­вре­мен­но пы­та­ют­ся эти дан­ные из­менить) дан­ные мо­гут ис­пор­тить­ся; в этом слу­чае необ­хо­ди­мо ис­поль­зо­вать до­полнитель­ные сред­ст­ва син­хрониза­ции, та­кие как бло­ки­ров­ка за­пи­сей в фай­ле, се­ма­фо­ры, об­мен со­об­щения­ми. И, на­конец, для мно­го­за­дач­но­сти в рас­пре­де­лен­ной сре­де у нас есть лишь од­но сред­ст­во взаи­мо­дей­ст­вия ме­ж­ду за­да­ча­ми – со­ке­ты (прав­да, кон­фи­гу­ра­ция со­ке­тов мо­жет быть раз­но­об­раз­ной – на­при­мер, со­ке­ты с уста­нов­лением со­единения и без).

По­сле об­ще­го об­зо­ра, что та­кое мно­го­за­дач­ность, и об­зо­ра средств для соз­дания мно­го­за­дач­ных при­ло­жений на при­ме­ре кон­крет­ной плат­фор­мы мы мо­жем пе­рей­ти непо­сред­ст­вен­но к основ­ной те­ме ста­тьи – мно­го­за­дач­но­сти в язы­ке Erlang. Язык Erlang из­на­чаль­но был раз­ра­бо­тан для соз­дания мно­го­за­дач­ных и рас­пре­де­лен­ных при­ло­жений, по­это­му сред­ст­ва для соз­дания за­дач и взаи­мо­дей­ст­вия ме­ж­ду ними име­ют под­держ­ку на уровне язы­ка (в чем очень ско­ро мы убе­дим­ся).

В от­ли­чие от боль­шин­ст­ва язы­ков и плат­форм, в язы­ке Erlang су­ще­ст­ву­ет толь­ко один тип за­дач, на­зы­вае­мый про­цес­са­ми. За­да­чи в язы­ке Erlang не яв­ля­ют­ся ис­тин­ны­ми про­цес­са­ми, т. к. не об­ла­да­ют соб­ст­вен­ным ад­рес­ным про­стран­ст­вом. Это оз­на­ча­ет, что несколь­ко про­цес­сов мо­гут вы­пол­нять­ся в пре­де­лах од­но­го эк­зем­п­ля­ра сре­ды вы­полнения Erlang (на од­ной вир­ту­аль­ной ма­шине Erlang). С дру­гой сто­ро­ны – и это роднит про­цес­сы Erlang с ис­тин­ны­ми про­цес­са­ми – про­цес­сы Erlang изо­ли­ро­ва­ны друг от дру­га (в от­ли­чие от, на­при­мер, по­то­ков в слу­чае мно­го­за­дач­но­сти, осно­ван­ной на по­то­ках). Изо­ли­ро­ван­ность про­цес­сов друг от дру­га за­клю­ча­ет­ся в сле­дую­щих ас­пек­тах. Во-пер­вых, дан­ные од­но­го про­цес­са не доступ­ны нико­му, ес­ли это­го не по­же­лал сам про­цесс. Во-вто­рых, ес­ли во вре­мя ра­бо­ты возника­ет необ­ра­ба­ты­вае­мое ис­клю­чение, то про­цесс Erlang бу­дет за­вер­шен (так же как и неко­то­рые свя­зан­ные с этим про­цес­сом про­цес­сы; но об этом – на од­ном из сле­дую­щих уро­ков). При этом за­вер­шение сбой­но­го про­цес­са Erlang не коснет­ся дру­гих (не свя­зан­ных с ним) про­цес­сов, вне за­ви­си­мо­сти от то­го, вы­пол­ня­ют­ся ли они в этом же эк­зем­п­ля­ре сре­ды вы­полнения Erlang или нет. Та­кое по­ве­дение от­ли­ча­ет­ся от по­ве­дения при возник­но­вении необ­ра­ба­ты­вае­мо­го ис­клю­чения в од­ном из по­то­ков вы­полнения ка­ко­го-ли­бо про­цес­са: в этом слу­чае обыч­но за­вер­ша­ет­ся про­цесс це­ли­ком, в том чис­ле и дру­гие по­то­ки вы­полнения.

Про­блем­ы в мно­го­за­дач­ных сре­дах
  • Го­ло­дание [starvation] За­держ­ка вре­мени от про­бу­ж­дения по­то­ка до его вы­зо­ва на про­цес­сор, в те­чение ко­то­рой он на­хо­дит­ся в спи­ске по­то­ков, го­то­вых к ис­полнению. Возника­ет по при­чине при­сут­ст­вия по­то­ков с боль­ши­ми или рав­ны­ми при­ори­те­та­ми, ко­то­рые ис­пол­ня­ют­ся все это вре­мя. Не­га­тив­ный эф­фект за­клю­ча­ет­ся в том, что возника­ет за­держ­ка вре­мени от про­бу­ж­дения по­то­ка до ис­полнения им сле­дую­щей важ­ной опе­ра­ции, что за­дер­жи­ва­ет ис­полнение этой опе­ра­ции, а сле­дом за ней и ра­бо­ту мно­гих дру­гих ком­понен­тов. Го­ло­дание соз­да­ет уз­кое ме­сто в сис­те­ме и не да­ет вы­жать из нее мак­си­маль­ную про­из­во­ди­тель­ность, ог­раничи­вае­мую толь­ко ап­па­рат­но обу­слов­лен­ны­ми уз­ки­ми мес­та­ми.
  • Гон­ки [race condition] Не­де­тер­миниро­ван­ный по­ря­док ис­полнения двух пу­тей ко­да, ра­бо­таю­щих с одними и те­ми же дан­ны­ми и ис­пол­няе­мы­ми в двух раз­лич­ных за­да­чах. При­во­дит к за­ви­си­мо­сти по­ряд­ка и пра­виль­но­сти ис­полнения от слу­чай­ных фак­то­ров.
  • Ин­вер­сия при­ори­те­та Пусть по­ток L име­ет низ­кий при­ори­тет, по­ток M – средний, по­ток H – вы­со­кий. Пусть по­ток L за­хва­тил объ­ект бло­ки­ров­ки (на­при­мер, мью­текс) и, вы­пол­ня­ясь с удер­жанием объ­ек­та бло­ки­ров­ки, пре­ры­ва­ет­ся про­бу­див­шим­ся по ка­кой-то при­чине по­то­ком M, ко­то­рый име­ет бо­лее вы­со­кий при­ори­тет. Пусть по­ток H так­же пы­та­ет­ся за­хва­тить этот объ­ект бло­ки­ров­ки. В такой си­туа­ции по­ток H ждет за­вер­шения те­ку­щей ра­бо­ты по­то­ком M, т. к., по­ка по­ток M ис­пол­ня­ет­ся, низ­ко­при­ори­тет­ный по­ток L не по­лу­ча­ет управ­ления и не мо­жет осво­бо­дить за­хва­чен­ный объ­ект бло­ки­ров­ки.

Про­цес­сы Erlang лег­ко­вес­ны, их соз­дание и за­вер­шение доста­точ­но де­ше­во (по сравнению, на­при­мер, с соз­данием про­цес­сов и по­то­ков в Linux), по­это­му во­прос о том, ис­поль­зо­вать или нет мно­го­за­дач­ность при ре­шении той или иной за­да­чи, дол­жен решать­ся толь­ко ис­хо­дя из фак­та, мож­но ли рас­па­рал­ле­лить ал­го­ритм за­да­чи или нет. Конеч­но, сле­ду­ет ска­зать, что воз­мож­ны си­туа­ции, когда при­менение мно­го­за­дач­но­го ре­шения бу­дет неоп­рав­дан­но (когда, на­при­мер, необ­хо­ди­мо несколь­ким за­да­чам ра­бо­тать од­но­вре­мен­но с боль­шим на­бо­ром дан­ных, рас­по­ло­жен­ных в па­мя­ти), но это­му мы по­свя­тим один из по­сле­дую­щих уро­ков.

Как уже го­во­ри­лось, в язы­ке Erlang у нас есть все­го один тип за­дач на все слу­чаи жизни – про­цес­сы Erlang. Мы уже по­ня­ли, что при желании ис­поль­зо­вать мно­го­за­дач­ность в рам­ках од­но­го эк­зем­п­ля­ра сре­ды вы­полнения Erlang доста­точ­но соз­дать необ­хо­ди­мое ко­ли­че­­ст­во про­цес­сов Erlang. Но что де­лать, ес­ли мы хо­тим соз­дать про­цес­сы Erlang на несколь­ких эк­зем­п­ля­рах сре­ды вы­полнения Erlang – как на локаль­ной ма­шине, так и в слу­чае рас­пре­де­лен­ной сис­те­мы? Для понимания это­го, да­вай­те вве­дем по­ня­тие уз­ла: узел – это име­но­ван­ный эк­зем­п­ляр сре­ды вы­полнения Erlang. Для соз­дания уз­ла доста­точ­но при ее за­пуске за­дать ко­рот­кое (при по­мо­щи клю­ча -sname) или длин­ное (при по­мо­щи клю­ча -name) имя уз­ла (эти­ми клю­ча­ми и разницей ме­ж­ду ними мы бо­лее под­роб­но зай­мем­ся на од­ном из сле­дую­щих уро­ков). По­сле то­го, как узел с оп­ре­де­лен­ным именем соз­дан, мы мо­жем соз­дать на нем необ­хо­ди­мое нам ко­ли­че­­ст­во про­цес­сов с неко­то­ро­го дру­го­го уз­ла. При этом соз­дание про­цес­са на ка­ком-ли­бо уз­ле прак­ти­че­­ски ничем не от­ли­ча­ет­ся от соз­дания про­цес­са в локаль­ной сре­де вы­полнения Erlang: для соз­дания про­цес­са на уз­ле нам необ­хо­ди­мо лишь ука­зать его имя, до­полнив рядом па­ра­мет­ров.

Мо­жет по­ка­зать­ся, что та­кая воз­мож­ность уда­лен­но­го соз­дания про­цес­сов – это боль­шая ды­ра в сис­те­ме безо­пас­но­сти. На са­мом де­ле это не так: сред­ст­ва­ми сре­ды вы­полнения Erlang мы мо­жем управ­лять тем, кто и ка­кой код мо­жет за­пускать на уда­лен­ном уз­ле, но об этом мы по­го­во­рим на од­ном из сле­дую­щих уро­ков.

Для понимания то­го, как соз­да­вать мно­го­за­дач­ные про­грам­мы в язы­ке Erlang, нам оста­лось по­го­во­рить еще на од­ну те­му – взаи­мо­дей­ст­вие ме­ж­ду за­да­ча­ми. Взаи­мо­дей­ст­вие ме­ж­ду за­да­ча­ми в язы­ке Erlang осу­ще­ст­в­ля­ет­ся при по­мо­щи об­ме­на со­об­щений. Про­цесс мо­жет по­слать лю­бо­му из­вест­но­му ему про­цес­су лю­бое со­об­щение – лю­бой объ­ект язы­ка Erlang. Что оз­на­ча­ет, что один про­цесс зна­ет о дру­гом про­цес­се? Это оз­на­ча­ет, что у про­цес­са есть иден­ти­фи­ка­тор (на­зы­вае­мый Pid) дру­го­го про­цес­са, ли­бо дру­гой про­цесс за­ре­ги­ст­ри­ро­вал свое имя, ко­то­рое зна­ет дан­ный про­цесс. Иден­ти­фи­ка­тор дру­го­го про­цес­са мы мо­жем знать, ес­ли мы сами его соз­да­ли ли­бо этот дру­гой про­цесс по­слал нам со­об­щение, со­дер­жа­щее его иден­ти­фи­ка­тор (ка­ж­дый про­цесс мо­жет лег­ко уз­нать свой иден­ти­фи­ка­тор). Со­об­щение от од­но­го про­цес­са дру­го­му по­сы­ла­ет­ся асин­хрон­но, т. е. мы не бло­ки­ру­ем про­цесс-отправитель, по­ка это со­об­щение не бу­дет достав­ле­но про­цес­су-получателю. По­сыл­ка со­об­щения от од­но­го про­цес­са дру­го­му про­цес­су яв­ля­ет­ся на­деж­ной; это оз­на­ча­ет, что сре­да вы­полнения Erlang га­ран­ти­ру­ет достав­ку со­об­щения по­лу­ча­те­лю.

Да­вай­те по­смот­рим, как у нас об­сто­ят де­ла со сто­ро­ны по­лу­чения со­об­щений. Ка­ж­дый про­цесс в язы­ке Erlang име­ет свя­зан­ную с ним оче­редь со­об­щений. Как толь­ко со­об­щение достав­ля­ет­ся сре­дой вы­полнения Erlang до про­цес­са, это со­об­щение раз­ме­ща­ет­ся в кон­це оче­ре­ди со­об­щений дан­но­го про­цес­са. Когда про­цесс пы­та­ет­ся по­лу­чить со­об­щение, удов­ле­тво­ряю­щее неко­то­рым кри­те­ри­ям, то он про­смат­ри­ва­ет по­сле­до­ва­тель­но свою оче­редь со­об­щений и из­вле­ка­ет из нее пер­вое удов­ле­тво­ряю­щее кри­те­ри­ям от­бо­ра со­об­щение. Ес­ли удов­ле­тво­ряю­ще­го кри­те­ри­ям от­бо­ра со­об­щения нет, то про­цесс, иниции­ро­вав­ший по­лу­чение, пе­ре­хо­дит в со­стояние ожи­дания. Это ожи­дание длит­ся до тех пор, по­ка сре­да вы­полнения Erlang не доста­вит про­цес­су ка­кое-ли­бо но­вое со­об­щение. Как толь­ко но­вое со­об­щение бу­дет достав­ле­но, про­цесс по­ис­ка со­об­щения, удов­ле­тво­ряю­ще­го кри­те­ри­ям от­бо­ра, бу­дет за­пу­щен за­но­во. По­нят­но, что про­цесс мо­жет бес­конеч­но ожи­дать удов­ле­тво­ряю­щее его кри­те­ри­ям со­об­щение; од­на­ко мы мо­жем по­вли­ять на мак­си­маль­ное вре­мя ожи­дания, за­дав его в вы­ра­жении по­лу­чения со­об­щения. Об­мен со­об­щения­ми ме­ж­ду про­цес­са­ми, несмот­ря на унифи­ка­цию мно­го­за­дач­но­сти в язы­ке Erlang, не един­ст­вен­ное сред­ст­во для взаи­мо­дей­ст­вия ме­ж­ду за­да­ча­ми. Так как, по­ми­мо об­щения друг с дру­гом, за­да­чи пред­на­зна­че­ны и для взаи­мо­дей­ст­вия с внешним ми­ром, то в на­ши ру­ки по­па­да­ют та­кие сред­ст­ва, как со­ке­ты, фай­лы и т. д.

По­сле об­су­ж­дения прин­ци­пов реа­ли­за­ции мно­го­за­дач­но­сти в язы­ке Erlang при­шла по­ра по­смот­реть на кон­крет­ные сред­ст­ва ее реа­ли­за­ции. Для соз­дания но­вых про­цес­сов у нас есть це­лое се­мей­ст­во BIF: spawn/1, spawn/2, spawn/3, spawn/4. Функ­ции spawn/1 и spawn/3 пред­на­зна­че­ны для соз­дания но­вых про­цес­сов на локаль­ном эк­зем­п­ля­ре сре­ды вы­полнения, функ­ции spawn/2 и spawn/4 – на уда­лен­ном уз­ле. Когда мы соз­да­ем но­вый про­цесс (од­ной из этих функ­ций), мы долж­ны ука­зать за­да­чу, ко­то­рую этот про­цесс бу­дет вы­пол­нять. За­да­чей всегда яв­ля­ет­ся неко­то­рая функ­ция. Функ­ции spawn/1 и spawn/2 за­да­ют вы­пол­няе­мую за­да­чу в ви­де ссыл­ки на про­из­воль­ную функ­цию. Функ­ции spawn/3 и spawn/4 за­да­ют вы­пол­няе­мую за­да­чу в ви­де MFA, где M – это мо­дуль, F – неко­то­рая экс­пор­ти­руе­мая из мо­ду­ля M функ­ция, A – спи­сок ар­гу­мен­тов, пе­ре­да­вае­мых дан­ной функ­ции. Разница ме­ж­ду эти­ми под­хо­да­ми при за­дании вы­пол­няе­мой за­да­чи со­сто­ит в том, что при по­мо­щи ссыл­ки на функ­цию мы мо­жем за­дать как экс­пор­ти­руе­мую функ­цию, так и аноним­ную функ­цию и не экс­пор­ти­руе­мую функ­цию из те­ку­ще­го мо­ду­ля; при по­мо­щи под­хо­да MFA мы всегда за­да­ем экс­пор­ти­руе­мую функ­цию.

Для соз­дания но­вых про­цес­сов у нас так­же име­ет­ся се­мей­ст­во BIF spawn_link. Разница ме­ж­ду этим се­мей­ст­вом функ­ций и се­мей­ст­вом функ­ций spawn в том, что се­мей­ст­во функ­ций spawn_link не толь­ко соз­да­ет но­вый про­цесс, но и соз­да­ет связь ме­ж­ду но­вым про­цес­сом и те­ку­щим (раз­го­вор о свя­зи ме­ж­ду про­цес­са­ми то­же ждет нас в бу­ду­щем). Все функ­ции обо­их се­мейств воз­вра­ща­ют иден­ти­фи­ка­тор про­цес­са или Pid.

Для от­прав­ки со­об­щения от од­но­го про­цес­са дру­го­му про­цес­су мы ис­поль­зу­ем вы­ра­жение Process! Message, где Process – вы­ра­жение, оп­ре­де­ляю­щий це­ле­вой про­цесс (зна­чением это­го вы­ра­жения яв­ля­ет­ся ли­бо иден­ти­фи­ка­тор про­цес­са, ли­бо имя за­ре­ги­ст­ри­ро­ван­но­го про­цес­са), а Message – от­прав­ляе­мое со­об­щение (лю­бой объ­ект язы­ка Erlang). Ре­зуль­та­том это­го вы­ра­жения от­прав­ки со­об­щения яв­ля­ет­ся зна­чение са­мо­го от­прав­ляе­мо­го со­об­щения Message. По­ми­мо это­го вы­ра­жения от­прав­ки со­об­щений, су­ще­ст­ву­ет еще це­лое се­мей­ст­во функ­ций, оп­ре­де­лен­ных в мо­ду­ле erlang для дан­ной за­да­чи. К это­му се­мей­ст­ву от­но­сят­ся функ­ции erlang:send/2, erlang:send/3, erlang:send_after/3, erlang:send_nosuspend/2, erlang:send_nosuspend/3. Эти функ­ции (за ис­клю­чением функ­ции erlang:send/2, ко­то­рая иден­тич­на вы­ра­жению от­прав­ки со­об­щения) по­зво­ля­ют на­стро­ить про­цесс от­прав­ки со­об­щений. Для ре­шения за­да­чи от­пра­вки со­об­щения у нас есть несколь­ко воз­мож­ных ва­ри­ан­тов, а для по­лу­чения со­об­щения толь­ко один – вы­ра­жение receive. Вы­ра­жение для по­лу­чения со­об­щений receive име­ет сле­дую­щий вид:

receive

Pattern1 [when GuardSeq1] -> Body1;

...;

PatternN [when GuardSeqN] -> BodyN

[after ExprT -> BodyT]

end

В этом вы­ра­жении по­лу­чения со­об­щения ка­ж­дое под­вы­ра­жение ви­да Patterni [when GuardSeqi] оп­ре­де­ля­ет кри­те­рий, по ко­то­ро­му то или иное со­об­щение из оче­ре­ди со­об­щений про­цес­са мо­жет быть вы­бра­но для об­ра­бот­ки. В под­вы­ра­жении Patterni [when GuardSeqi] часть Patterni яв­ля­ет­ся вы­ра­жением со­от­вет­ст­вия шаб­ло­ну [pattern-matching], а часть when GuardSeqi яв­ля­ет­ся вы­ра­жением ох­ра­ны. Ес­ли бу­дет най­де­но со­об­щение, удов­ле­тво­ряю­щее од­но­му из кри­те­ри­ев Patterni [when GuardSeqi], то зна­чением вы­ра­жения receive бу­дет под­вы­ра­жение Bodyi, со­от­вет­ст­вую­щее под­вы­ра­жению кри­те­рия вы­бо­ра. Вы­ра­жение after ExprT -> BodyT за­да­ет по­ве­дение, ес­ли в оче­ре­ди со­об­щений не ока­за­лось под­хо­дя­ще­го со­об­щения: в этом слу­чае вы­полнение про­цес­са приоста­нав­ли­ва­ет­ся мак­си­мум на ExprT мил­ли­се­кунд. Ес­ли в те­чение ExprT мил­ли­се­кунд в оче­ре­ди со­об­щений про­цес­са не поя­вит­ся со­об­щения, удов­ле­тво­ряю­ще­го пе­ре­чис­лен­ным вы­ше кри­те­ри­ям, то про­цесс за­кон­чит ожи­дание под­хо­дя­ще­го со­об­щения, и зна­чением вы­ра­жения receive бу­дет под­вы­ра­жение BodyT. Зна­чением под­вы­ра­жения ExprT так­же мо­жет быть атом infinity; в этом слу­чае про­цесс бу­дет ждать со­об­щения, удов­ле­тво­ряю­ще­го од­но­му из кри­те­ри­ев, бес­конеч­но дол­го. Вы­ра­жение after ExprT -> BodyT не яв­ля­ет­ся обя­за­тель­ным; ес­ли оно от­сут­ст­ву­ет в вы­ра­жении receive, то это эк­ви­ва­лент­но слу­чаю бес­конеч­но­го ожи­дания под­хо­дя­ще­го со­об­щения про­цес­сом.

А те­перь при­шла по­ра прак­ти­ки: да­вай­те на про­стом при­ме­ре по­смот­рим, как ра­бо­тать с мно­го­за­дач­но­стью в язы­ке Erlang. В этом при­ме­ре мы из основ­но­го про­цес­са соз­да­дим ра­бо­чий про­цесс и бу­дем взаи­мо­дей­ст­во­вать с ним при по­мо­щи со­об­щений. Да­вай­те рас­смот­рим сна­ча­ла функ­цию (вы­пол­няе­мую за­да­чу) ра­бо­че­го про­цес­са example_worker/1:

example_worker(Master) ->

receive

{ping, From } ->

io:format(“ping from master ~n”),

From ! {pong, self()},

example_worker(Master);

_Other ->

io:format(“unknown message ~n”),

example_worker(Master)

after 1000 ->

io:format(“timeout ~n”),

erlang:send(Master, timeout)

end.

Эта функ­ция со­дер­жит вы­ра­жение для по­лу­чения со­об­щений receive. Dы­ра­жение receive уме­ет по­лу­чать со­об­щения ви­да {ping, From}, где From – лю­бой объ­ект язы­ка Erlang (мы на­де­ем­ся, что From – это иден­ти­фи­ка­тор про­цес­са от­пра­ви­те­ля, но при необ­хо­ди­мости это мо­жно про­ве­рить яв­но при по­мо­щи BIF is_pid/1) и все осталь­ные со­об­щения. Обыч­но все осталь­ные со­об­щения (т. е. со­об­щения, не под­па­даю­щие ни под один дру­гой кри­те­рий) по­лу­ча­ют для то­го, что­бы эти «му­сор­ные» со­об­щения не на­ка­п­ли­ва­лись в оче­ре­ди со­об­щения про­цес­са. Кро­ме то­го, вы­ра­жение receive со­дер­жит сек­цию after, ко­то­рая за­да­ет мак­си­маль­ное вре­мя ожи­дания в 1000 мил­ли­се­кунд.

При по­лу­чении лю­бо­го со­об­щения мы вы­во­дим ин­фор­ма­цию на эк­ран и, при по­мо­щи хво­сто­вой ре­кур­сии, за­цик­ли­ва­ем вы­полнение функ­ции example_worker/1. Та­ким об­ра­зом, функ­ция example_worker/1 яв­ля­ет­ся функ­ци­ей об­ра­бот­ки со­об­щений. Кро­ме то­го, при по­лу­чении со­об­щения {ping, From} мы от­сы­ла­ем про­цес­су, по­слав­ше­му нам это со­об­щение, со­об­щение {pong, self()}. BIF self/0 воз­вра­ща­ет иден­ти­фи­ка­тор те­ку­ще­го про­цес­са, но у нас нет ника­ко­го спо­со­ба уз­нать иден­ти­фи­ка­тор про­цес­са, по­слав­ше­го нам со­об­щение, кро­ме как вклю­чить этот иден­ти­фи­ка­тор в са­мо со­об­щение. Имен­но этого мы ожи­да­ем при по­лу­чении со­об­щения {ping, From}, и имен­но это мы де­ла­ем, когда от­сы­ла­ем со­об­щение {pong, self()}.

И, на­конец, при на­сту­п­лении тай­мау­та в вы­ра­жении receive мы от­сы­ла­ем со­об­щение timeout глав­но­му про­цес­су при по­мо­щи функ­ции erlang:send/2 (для де­мон­ст­ра­ции та­кой воз­мож­но­сти) и об­ры­ва­ем цикл об­ра­бот­ки со­об­щений.

Те­перь да­вай­те взгля­нем на функ­цию глав­но­го про­цес­са example/0:

example() ->

Master = self(),

Worker = spawn(fun() -> example_worker(Master) end),

Worker ! hello,

Worker ! {ping, Master},

Worker ! hi,

receive

timeout -> io:format(“timeout in worker ~n”)

end,

receive

{pong, Worker} -> io:format(“pong from worker ~n”)

end.

В этой функ­ции мы соз­да­ем ра­бо­чий про­цесс (при по­мо­щи BIF spawn/1, ко­то­рая воз­вра­ща­ет иден­ти­фи­ка­тор соз­дан­но­го про­цес­са), по­сле че­го по­сы­ла­ем ра­бо­че­му про­цес­су ряд со­об­щений. Сре­ди этих со­об­щений на­хо­дит­ся как из­вест­ное ра­бо­че­му про­цес­су со­об­щение {ping, Master}, так и неиз­вест­ные hello и hi. По­сле это­го, при по­мо­щи вы­ра­жений receive, мы принима­ем со­об­щения от ра­бо­че­го по­то­ка. За­меть­те, что принимать со­об­щения мы мо­жем в лю­бом по­ряд­ке, а не в том, в ко­то­ром они бы­ли по­сла­ны. Ес­ли мы за­пустим на вы­полнение функ­цию example/0 (соз­дав мо­дуль с экс­пор­ти­руе­мой функ­ци­ей example/0), то мы по­лу­чим сле­дую­щий вы­вод в кон­со­ли сре­ды вы­полнения Erlang:

unknown message

ping from master

unknown message

timeout

timeout from worker

pong from worker

Мы се­го­дня сде­ла­ли пер­вый, но очень важ­ный шаг в сто­ро­ну мно­го­за­дач­но­сти в язы­ке Erlang. На­пи­сание мно­го­за­дач­ных про­грамм – доста­точ­но слож­ная об­ласть, как в плане ал­го­рит­мов, так и в плане техниче­­ских средств. Язык Erlang по­зво­ля­ет снять слож­ность техниче­­ско­­го пла­на и по­зво­ля­ет нам со­сре­до­то­чить­ся толь­ко на со­от­вет­ст­вую­щих мно­го­за­дач­ных ал­го­рит­мах. Мы это уви­де­ли се­го­дня (пускай это бы­ло не очень оче­вид­но), мы это уви­дим еще не раз. А на сле­дую­щем уро­ке мы по­го­во­рим о сред­ст­вах для соз­дания устой­чи­вых к ошиб­кам про­грамм. |

Персональные инструменты
купить
подписаться
Яндекс.Метрика