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

LXF161:Erlang

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


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

Erlang: О поль­зе биб­лио­тек

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

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

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

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

Функ­ции, ис­поль­зую­щие со­стояние

При­ме­ром функ­ций, ис­поль­зую­щих со­стояние, со­хранен­ное ме­ж­ду их вы­зо­ва­ми вы­зы­ваю­щей сто­ро­ной, яв­ля­ют­ся функ­ции для рас­че­та зна­чения хэ­ша MD5 (оп­ре­де­лен­ные в мо­ду­ле erlang). Функ­ция erlang:md5_init() инициа­ли­зи­ру­ет со­стояние, на­зы­вае­мое кон­тек­стом Context, и воз­вра­ща­ет его.

Функ­ция erlang:md5_update(Context, Data) принима­ет в ка­че­­ст­ве ар­гу­мен­тов ста­рое со­стояние Context и оче­ред­ную пор­цию дан­ных Data, и воз­вра­ща­ет об­нов­лен­ное со­стояние. Функ­ция erlang:md5_final(Context) принима­ет со­стояние Context и воз­вра­ща­ет зна­чение хэ­ша MD5.

Описанный под­ход к хранению со­стояния яв­ля­ет­ся стро­го функ­цио­наль­ным. Сре­да вы­полнения Erlang со­дер­жит и дру­гой под­ход к хранению со­стояния: это ис­поль­зо­вание па­мя­ти, при­дан­ной про­цес­су. С ка­ж­дым про­цес­сом свя­зан некий объ­ем па­мя­ти в ви­де из­ме­няе­мо­го сло­ва­ря; доступ к этой па­мя­ти име­ет толь­ко код, вы­пол­няе­мый про­цес­сом. Ес­ли два раз­ных про­цес­са вы­пол­ня­ют один и тот же код, ко­то­рый об­ра­ща­ет­ся к па­мя­ти, свя­зан­ной с про­цес­сом, то этот код бу­дет об­ра­щать­ся к раз­ным об­лас­тям па­мя­ти, в за­ви­си­мо­сти от то­го, ка­кой из про­цес­сов его вы­пол­ня­ет. Для ра­бо­ты с такой па­мя­тью в язы­ке Erlang оп­ре­де­лен це­лый ряд функ­ций (а точ­нее, BIF). Со­хранить неко­то­рое зна­чение Value и ас­со­ции­ро­ван­ный с ним ключ Key (при по­мо­щи ко­то­ро­го бу­дет осу­ще­ст­в­лять­ся доступ к это­му зна­чению в дальней­шем) мож­но при по­мо­щи функ­ции put(Key, Value). Ес­ли с дан­ным клю­чом уже бы­ло ас­со­ции­ро­ва­но ка­кое-ли­бо зна­чение, оно бу­дет воз­вра­щае­мым зна­чением функ­ции put/2 (ина­че воз­вра­щае­мым зна­чением бу­дет атом undefined). Для по­лу­чения зна­чения Value по клю­чу Key слу­жит функ­ция get(Key), ко­то­рая воз­вра­ща­ет ли­бо зна­чение Value, ас­со­ции­ро­ван­ное с клю­чом Key, ли­бо атом undefined. Мы мо­жем по­лу­чить все дан­ные сра­зу, в ви­де пар ключ – зна­чение: для это­го слу­жит функ­ция get/0. По­ми­мо это­го, мо­жно по­лу­чить спи­сок всех клю­чей, ас­со­ции­ро­ван­ных со зна­чением Value, при по­мо­щи функ­ции get_keys(Value). И, на­конец, что­бы уда­лить неко­то­рое зна­чение по клю­чу (и ас­со­ции­ро­ван­ный с ним ключ), ис­поль­зу­ет­ся функ­ция erase(Key); что­бы пол­но­стью очи­стить па­мять, свя­зан­ную с про­цес­сом – функ­ция erase/0.

Сле­ду­ет сра­зу ска­зать, что ис­поль­зо­вание па­мя­ти, свя­зан­ной с про­цес­сом и этим на­бо­ром функ­ций, име­ет два боль­ших ми­ну­са по сравнению с «клас­си­че­­ским» функ­цио­наль­ным под­хо­дом. Во-пер­вых, мож­но лег­ко на­пи­сать функ­цию, воз­вра­щае­мое зна­чение ко­то­рой при одних и тех же ар­гу­мен­тах бу­дет раз­ли­чать­ся, что на­ру­ша­ет по­ло­жение функ­цио­наль­но­го про­грам­ми­ро­вания о том, что ре­зуль­тат вы­полнения функ­ции дол­жен за­ви­сеть толь­ко от ее ар­гу­мен­тов. Конеч­но, есть ряд функ­ций, ко­то­рые это по­ло­жение на­ру­ша­ют, но все эти функ­ции яв­ля­ют­ся ин­тер­фей­сом к внешнему ми­ру – на­при­мер, функ­ция now/0, воз­вра­щаю­щая те­ку­щее вре­мя. Во-вто­рых, ис­поль­зо­вание па­мя­ти, свя­зан­ной с про­цес­сом, соз­да­ет неяв­ную связь как ме­ж­ду функ­ци­ей и неко­то­рым со­стоянием, так и ме­ж­ду несколь­ки­ми функ­ция­ми. Это уве­ли­чи­ва­ет связ­ность и слож­ность при­ло­жения. В об­щем, ис­поль­зо­вать па­мять, свя­зан­ную с про­цес­сом, или нет – ре­шать, конеч­но, вам.

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

Начнем с раз­го­во­ра о ли­де­ре груп­пы про­цес­сов. Все про­цес­сы в язы­ке Erlang при­над­ле­жат той или иной груп­пе, и в ка­ж­дой груп­пе есть про­цесс, на­зы­вае­мый ли­де­ром этой груп­пы. Весь ввод/вы­вод внут­ри груп­пы на­прав­ля­ет­ся из/в ли­де­ра груп­пы. Когда один про­цесс соз­да­ет дру­гой про­цесс, то у но­во­го про­цес­са бу­дет тот же са­мый ли­дер груп­пы (и та же са­мая груп­па), что и у ро­ди­тель­ско­го про­цес­са. Для по­лу­чения иден­ти­фи­ка­то­ра ли­де­ра груп­пы, ко­то­рой при­над­ле­жит те­ку­щий про­цесс, сле­ду­ет ис­поль­зо­вать функ­цию group_leader/0. Что­бы для груп­пы, ко­то­рой при­над­ле­жит про­цесс с иден­ти­фи­ка­то­ром Pid, за­дать но­во­го ли­де­ра, сле­ду­ет ис­поль­зо­вать функ­цию group_leader(GroupLeaderPid, Pid), где GroupLeaderPid – иден­ти­фи­ка­тор про­цес­са, ко­то­рый станет но­вым ли­де­ром груп­пы.

Отметим так­же, что для управ­ления груп­па­ми про­цес­сов сле­ду­ет ис­поль­зо­вать функ­ции из мо­ду­лей pg или pg2. Мо­дуль erlang, по­ми­мо вы­шеупомянутых функ­ций, со­дер­жит так­же функ­ции, реа­ли­зую­щие функ­цио­наль­ность тай­ме­ра: erlang:start_timer/3, erlang:read_timer/1, erlang:cancel_timer/1 и erlang:send_after/3 (о них мы по­го­во­рим под­роб­нее в од­ном из прак­ти­ку­мов).

Сле­дую­щий мо­дуль, ко­то­рым мы займемся – неболь­шой мо­дуль lib, со­дер­жа­щий раз­но­об­раз­ные по­лез­ные функ­ции. Из этих функ­ций для нас ин­те­рес­ны (в клю­че на­шей ста­тьи) в пер­вую оче­редь две: lib:flush_receive/0 и lib:sendw/2 (так­же этот мо­дуль со­дер­жит функ­цию lib:send/2, пол­но­стью иден­тич­ную функ­ции erlang:send/2). Функ­ция lib:flush_receive/0 слу­жит для очи­ст­ки оче­ре­ди со­об­щений про­цес­са от необ­ра­бо­тан­ных со­об­щений. Мы помним, что ка­ж­дый про­цесс со­дер­жит соб­ст­вен­ную оче­редь со­об­щений. Когда мы по­сы­ла­ем со­об­щение неко­то­ро­му про­цес­су, оно по­ме­ща­ет­ся в оче­редь со­об­щений это­го про­цес­са.

Вы­ра­жение receive из­вле­ка­ет пер­вое под­хо­дя­щее со­об­щение из оче­ре­ди со­об­щений, остав­ляя осталь­ные со­об­щения в оче­ре­ди. По­это­му вполне воз­мож­но возник­но­вение си­туа­ции, когда ко­ли­че­­ст­во со­об­щений в оче­ре­ди ка­ко­го-ли­бо про­цес­са по­сто­ян­но уве­ли­чи­ва­ет­ся, что, в конеч­ном ито­ге, при­ве­дет к за­мед­лению вы­чис­ления (вы­полнения) вы­ра­жения receive и пе­ре­полнению оче­ре­ди со­об­щений. Та­кая си­туа­ция воз­мож­на, ес­ли про­цесс по­лу­ча­ет со­об­щения, не под­хо­дя­щие ни для од­но­го вы­ра­жения receive, ли­бо ес­ли со­об­щения при­хо­дят бы­ст­рее, чем про­цесс их об­ра­ба­ты­ва­ет. Пер­вого случая мож­но из­бе­жать, ес­ли в вы­ра­жении receive всегда до­бав­лять в конец ва­ри­ант, об­ра­ба­ты­ваю­щий все со­об­щения. Од­на­ко этот под­ход не будет рабо­тать, ес­ли вид вы­ра­жения receive (ва­ри­ан­ты об­ра­ба­ты­вае­мых со­об­щений) мо­жет из­ме­нять­ся (на­при­мер, в за­ви­си­мо­сти от неко­то­ро­го со­стояния). В этом слу­чае (а так­же в слу­чае пе­ре­полнения оче­ре­ди со­об­щений) мо­жно восполь­зо­вать­ся функ­ци­ей lib:flush_receive/0. Что­бы уз­нать, сколь­ко же со­об­щений со­дер­жит­ся в оче­ре­ди со­об­щений про­цес­са, необ­хо­ди­мо ис­поль­зо­вать BIF process_info(Pid, [message_queue_len]), где Pid – иден­ти­фи­ка­тор про­цес­са, раз­мер оче­ре­ди со­об­щений ко­то­ро­го мы хо­тим уз­нать.

Мы так­же мо­жем по­лу­чить спи­сок всех необ­ра­бо­тан­ных со­об­щений в оче­ре­ди со­об­щений про­цес­са, вы­з­вав process_info(Pid, [messages]). Функ­ция lib:sendw(To, Msg) от­сы­ла­ет со­об­щение про­цес­су To и ожи­да­ет от­вет­но­го со­об­щения, ко­то­рое она и воз­вра­ща­ет. Эта функ­ция по­зво­ля­ет уп­ро­стить на­пи­сание ко­да для син­хрониза­ции взаи­мо­дей­ст­вия ме­ж­ду про­цес­са­ми, когда тре­бу­ет­ся по­слать со­об­щение про­цес­су, до­ж­дать­ся от него от­ве­та и про­дол­жить вы­полнение за­да­чи.

У этой функ­ции есть два недостат­ка, о ко­то­рых необ­хо­ди­мо помнить при ее ис­поль­зо­вании. Во-пер­вых, она ждет от­ве­та от дру­го­го про­цес­са бес­конеч­но дол­го; ес­ли от­вет от дру­го­го про­цес­са по ка­ким-ли­бо при­чи­нам не при­дет ожи­даю­ще­му про­цес­су, то ожи­даю­щий про­цесс бу­дет за­бло­ки­ро­ван на­всегда. Во-вто­рых, эта функ­ция об­ра­ба­ты­ва­ет пер­вое со­об­щение от лю­бо­го про­цес­са, ко­то­рое при­дет по­сле по­сыл­ки со­об­щения ка­ко­му-то оп­ре­де­лен­но­му про­цес­су. Вполне воз­мож­на си­туа­ция, что мы по­сы­ла­ем со­об­щение од­но­му про­цес­су, пер­вым при­хо­дит со­об­щение от дру­го­го про­цес­са, и имен­но его нам функ­ция lib:sendw/2 и воз­вра­тит.

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

Начнем с функ­ций, по­зво­ля­ющих вы­полнить неко­то­рую функ­цию на уда­лен­ной сто­роне и вер­нуть ре­зуль­тат вы­полнения этой функ­ции: rpc:call(Node, Module, Func, Args) и rpc:call(Node, Module, Func, Args, Timeout). Обе эти функ­ции на уз­ле Node в от­дель­ном про­цес­се вы­пол­ня­ют функ­цию apply(Module, Func, Args), т. е. вы­зы­ва­ют функ­цию Module:Func с ар­гу­мен­та­ми Args и воз­вра­ща­ют ре­зуль­тат это­го вы­зо­ва. Ес­ли вы­зов функ­ции был успеше­н, то ре­зуль­тат вы­зо­ва воз­вра­ща­ет­ся на­пря­мую; ес­ли же во вре­мя вы­зо­ва возник­ла ошиб­ка (ис­клю­чение вре­мени вы­полнения), то эти функ­ции в ка­че­­ст­ве ре­зуль­та­та вер­нут кор­теж {badrpc, Reason}, где Reason – при­чи­на возник­но­вения ошиб­ки. Обе эти функ­ции син­хрон­ные – то есть вы­зы­ваю­щий про­цесс бло­ки­ру­ет­ся до по­лу­чения ре­зуль­тата. Един­ст­вен­ное от­ли­чие ме­ж­ду эти­ми дву­мя функ­ция­ми – пер­вая функ­ция ожи­да­ет ре­зуль­та­та бес­конеч­но дол­го, тогда как вто­рая – в те­чение от­рез­ка вре­мени, за­дан­но­го па­ра­мет­ром Timeout.

Функ­ции rpc:block_call/4 и rpc:block_call/5 ана­ло­гич­ны двум пре­ды­ду­щим функ­ци­ям, за одним ис­клю­чением: в от­ли­чие от пре­ды­ду­щих, они не соз­да­ют но­вый про­цесс на уз­ле Node, а вы­пол­ня­ют вы­зов в про­цес­се rpc-сер­ве­ра. Это при­во­дит к то­му, что rpc-сер­вер на уз­ле Node бло­ки­ру­ет­ся до окон­чания вы­полнения ис­ход­но­го вы­зо­ва. По­лу­ча­ет­ся, что по­ка на уз­ле Node вы­зов функ­ции rpc:block_call/4 или rpc:block_call/5 не за­вер­шит­ся, ника­кой дру­гой вы­зов ка­кой-ли­бо функ­ции из мо­ду­ля rpc на уз­ле Node вы­пол­нять­ся не начнет. Од­на­ко есть неко­то­рые тон­ко­сти, свя­зан­ные с эти­ми функ­ция­ми. Пред­по­ло­жим, что на уз­ле Node мы иниции­ро­ва­ли вы­полнение функ­ции rpc:call/4 (или лю­бой дру­гой функ­ции, от­лич­ной от функ­ций rpc:block_call/4 и rpc:block_call/5). По­сле это­го из дру­го­го про­цес­са мы иниции­ро­ва­ли вы­полнение функ­ции rpc:block_call/4 (или rpc:block_call/5) опять же на уз­ле Node, что при­ве­дет к бло­ки­ро­ванию rpc-сер­ве­ра на уз­ле Node. Но при этом вы­зов функ­ции rpc:call/4 про­дол­жит вы­пол­нять­ся на уз­ле Node, т. к. он вы­пол­ня­ет­ся в сво­ем про­цес­се (а не в про­цес­се rpc-сер­ве­ра). Да­лее пред­по­ло­жим, что вы­зов функ­ции rpc:call/4 за­вер­ша­ет­ся рань­ше вы­зова функ­ции rpc:block_call/4.

По за­вер­шении вы­зо­ва функ­ции rpc:call/4 вы­зы­ваю­щая сто­ро­на долж­на по­лу­чить ре­зуль­тат ра­бо­ты этой функ­ции; од­на­ко ре­зуль­тат пе­ре­да­ет­ся не на­пря­мую, а че­рез rpc-сер­вер, что при­ве­дет к то­му, что ре­зуль­тат вы­зо­ва функ­ции rpc:call/4 бу­дет пе­ре­дан вы­зы­ваю­щей сто­роне толь­ко по­сле за­вер­шения вы­зо­ва функ­ции rpc:block_call/4. Ну и никто не за­пре­ща­ет нам соз­да­вать про­цес­сы на уз­ле Node при по­мо­щи од­ной из функ­ций се­мейств spawn/2,4 и spawn_link/2,4, да­же ес­ли в этот мо­мент на уз­ле Node вы­пол­ня­ет­ся вы­зов од­ной из функ­ций rpc:block_call/4 и rpc:block_call/5.

«Не­под­хо­дя­щие» объ­ек­ты язы­ка

При ис­поль­зо­вании функ­ций се­мей­ст­ва global:set_lock/1,2,3 для уста­нов­ления бло­ки­ров­ки при­ме­ня­ет­ся иден­ти­фи­ка­тор бло­ки­ров­ки, имею­щий вид {ResourceId, LockRequesterId}, где ResourceId – иден­ти­фи­ка­тор за­пра­ши­вае­мо­го ре­сур­са, LockRequesterId – иден­ти­фи­ка­тор сто­ро­ны, за­пра­ши­ваю­щей бло­ки­ров­ку на ре­сурс. В ка­че­­ст­ве как ResourceId, так и LockRequesterId мо­жет вы­сту­пать лю­бой объ­ект язы­ка Erlang. Но на ве­ли­чи­ну ResourceId есть ог­раничения: ато­мы global, dist_ac, mnesia_adjust_log_writes, pg2, mnesia_table_lock не го­дятся как иден­ти­фи­ка­то­ры ре­сур­сов при уста­нов­лении бло­ки­ров­ки, и их применять не ре­ко­мен­ду­ет­ся.

Пред­по­ло­жим, что пе­ред на­ми сто­ит за­да­ча вы­полнить уда­лен­ный вы­зов функ­ции и по­лу­чить ре­зуль­тат это­го вы­зо­ва сра­зу на несколь­ких уз­лах. Конеч­но, мы мо­жем ее ре­шить, по­сле­до­ва­тель­но вы­зы­вая функ­цию rpc:call/4 для ка­ж­до­го уз­ла, где необ­хо­ди­мо уда­лен­но вы­полнить функ­цию и по­лу­чить ре­зуль­тат. Вполне оче­вид­но, что та­кое ре­шение неэф­фек­тив­но: ка­ж­дый сле­дую­щий вы­зов функ­ции rpc:call/4 мы смо­жем сде­лать толь­ко когда пре­ды­ду­щий вы­зов вернет зна­чение, а за­да­ча, что оче­вид­но, лег­ко рас­па­рал­ле­ли­ва­ет­ся. Но нам нет нужды реа­ли­зо­вы­вать та­кое па­рал­лель­ное ре­шение; для это­го мо­дуль rpc со­дер­жит функ­ции rpc:multicall/4 и rpc:multicall/5. Функ­ция rpc:multicall(Nodes, Module, Func, Args, Timeout) вы­чис­ля­ет зна­чение функ­ции Module:Func с ар­гу­мен­та­ми Args на уз­лах из спи­ска Nodes и воз­вра­ща­ет ре­зуль­тат в ви­де {ResL, BadNodes}, где ResL – спи­сок по­лу­чен­ных ре­зуль­та­тов, BadNodes – спи­сок уз­лов, на ко­то­рых вы­чис­ление функ­ции окон­чи­лось неуда­чей. Па­ра­метр Timeout в функ­ции rpc:multicall/5 уста­нав­ли­ва­ет мак­си­маль­ное вре­мя ожи­дания от­ве­та от ка­ж­до­го из уз­лов. Ес­ли за вре­мя Timeout от ка­ко­го-ли­бо уз­ла от­ве­та не по­лу­че­но, узел по­па­да­ет в спи­сок BadNodes, т. е. мы счи­та­ем, что вы­чис­ле­ние функ­ции на дан­ном уз­ле окон­чи­лось неуда­чей. Спи­сок по­лу­чен­ных ре­зуль­та­тов ResL со­дер­жит ре­зуль­та­ты вы­чис­ления функ­ции Module:Func с ар­гу­мен­та­ми Args в про­из­воль­ном по­ряд­ке (от­но­си­тель­но по­ряд­ка уз­лов в спи­ске Nodes) и без при­вяз­ки к уз­лу, на ко­то­ром этот ре­зуль­тат был по­лу­чен.

Ес­ли та­кая при­вяз­ка нуж­на, то са­ма функ­ция долж­на ее воз­вра­щать – на­при­мер, в ви­де кор­те­жа, со­стоя­ще­го из имени уз­ла и вы­чис­лен­но­го ре­зуль­та­та. По­нят­но, что функ­ция rpc:multicall/5 (и rpc:multicall/4) воз­вра­ща­ет управ­ление толь­ко тогда, когда бу­дет сфор­ми­ро­ван ре­зуль­тат, т. е. яв­ля­ет­ся син­хрон­ной функ­ци­ей.

Все вышеописанные функ­ции из мо­ду­ля rpc бы­ли син­хрон­ны­ми функ­ция­ми, но вы­зов син­хрон­ной функ­ции при­во­дит к бло­ки­ро­ванию вы­зы­ваю­ще­го про­цес­са, что не всегда же­ла­тель­но. А значит, необ­хо­дим асин­хрон­ный вы­зов функ­ции на уда­лен­ном уз­ле: для это­го мы сна­ча­ла вы­зы­ва­ем функ­цию rpc:acync_call/4, ко­то­рая воз­вра­ща­ет ключ, ис­поль­зуе­мый по­том для по­лу­чения ре­зуль­та­та вы­зо­ва при по­мо­щи од­ной из функ­ций rpc:yield/1, rpc:nb_yield/1 или rpc:nb_yield/2. Функ­ция rpc:yield/1 бло­ки­ру­ет вы­зы­ваю­щий про­цесс до по­лу­чения ре­зуль­та­та (ес­ли ре­зуль­тат вы­зо­ва уже го­тов, то ника­кой бло­ки­ров­ки не бу­дет). Функ­ция rpc:nb_yield(Key, Timeout) жд­ет го­тов­но­сти ре­зуль­та­та вы­зо­ва согласно па­ра­мет­ру Timeout и воз­вра­ща­ет ли­бо ре­зуль­тат (точнее, кор­теж {value, Value}, где Value – ре­зуль­тат вы­зо­ва), ли­бо атом timeout; функ­ция rpc:nb_yield/1 эк­ви­ва­лент­на вы­зо­ву rpc:nb_yield(Key, 0).

Да­вай­те ко­рот­ко по­го­во­рим об остав­ших­ся функ­ци­ях из мо­ду­ля rpc. Ес­ли нам воз­вра­щае­мое зна­чение не нуж­но (обыч­но это функ­ции с неко­то­рым по­боч­ным эф­фек­том), то для уда­лен­но­го вы­зо­ва мож­но восполь­зо­вать­ся функ­ци­ей rpc:cast/4, ко­то­рая не бло­ки­ру­ет вы­полнение вы­зы­ваю­ще­го по­то­ка. Ес­ли необ­хо­ди­мо вы­чис­лить зна­чение функ­ции, воз­вра­щае­мое зна­чение ко­то­рой нас не ин­те­ре­су­ет, на несколь­ких уз­лах, для это­го мож­но восполь­зо­вать­ся од­ной из функ­ций rpc:eval_everywhere/3,4.

Пред­по­ло­жим, что на неко­то­ром на­бо­ре уз­лов у нас есть про­цес­сы (на ка­ж­дом уз­ле) с оди­на­ко­вым локаль­ным за­ре­ги­ст­ри­ро­ван­ным именем Name. Ес­ли мы хо­тим по­слать всем этим про­цес­сам со­об­щение син­хрон­но, то для это­го мо­дуль rpc со­дер­жит функ­цию rpc:sbcast(Nodes, Name, Msg) (а так­же функ­цию rpc:sbcast/2). Эта функ­ция шлет со­об­щение Msg про­цес­сам с локаль­ным именем Name на уз­лах из спи­ска Nodes и воз­вра­ща­ет кор­теж {GoodNodes, BadNodes}, где GoodNodes – спи­сок уз­лов, на ко­то­рых су­ще­ст­ву­ет про­цесс с именем Name, BadNodes – спи­сок уз­лов, на ко­то­рых про­цес­са с именем Name нет. Эта функ­ция га­ран­ти­ру­ет толь­ко, что со­об­щение Msg бу­дет по­сла­но всем су­ще­ст­вую­щим про­цес­сам с локаль­ным именем Name на уз­лах из спи­ска Nodes, но не об­ра­бо­та­но эти­ми про­цес­са­ми. Ес­ли мы хо­тим вы­полнить ту же са­мую за­да­чу, но асин­хрон­но, то к на­шим услу­гам в мо­ду­ле rpc есть функ­ции rpc:abcast/2 и rpc:abcast/3.

Пре­ды­ду­щие функ­ции по­зво­ля­ли про­сто по­слать неко­то­рое со­об­щение уда­лен­но­му про­цес­су и не ожи­да­ли ника­ко­го от­ве­та от него (хо­тя, вклю­чив в со­об­щение иден­ти­фи­ка­тор про­цес­са от­пра­ви­те­ля, вполне мож­но реа­ли­зо­вать и по­лу­чение от­вет­но­го со­об­щения от уда­лен­но­го про­цес­са). Ес­ли же на­ша за­да­ча со­сто­ит в том, что­бы по­слать со­об­щение и по­лу­чить от­вет, то для этой це­ли мо­дуль rpc со­дер­жит еще ряд функ­ций. Функ­ция rpc:server_call(Node, Name, ReplyWrapper, Msg) пред­на­зна­че­на для син­хрон­ной по­сыл­ки со­об­щения Msg про­цес­су с локаль­ным именем Name, за­ре­ги­ст­ри­ро­ван­ным на уз­ле Node. При этом уда­лен­ный про­цесс дол­жен ожи­дать со­об­щение в ви­де {From, Msg}, где From – иден­ти­фи­ка­тор про­цес­са инициа­то­ра, и от­сы­лать от­вет­ное со­об­щение об­рат­но в ви­де {ReplyWrapper, Node, Reply}. Здесь Reply – это ре­зуль­тат вы­полнения за­про­са; это ли­бо про­сто неко­то­рый объ­ект язы­ка Erlang, ли­бо кор­теж {error, Reason} в слу­чае возник­но­вения ошиб­ки. Ес­ли мы хо­тим ото­слать со­об­щения несколь­ким про­цес­сам с локаль­ным именем Name, за­ре­ги­ст­ри­ро­ван­ным на раз­ных уз­лах, то для это­го в мо­ду­ле rpc оп­ре­де­ле­ны функ­ции rpc:multi_server_call/2 и rpc:multi_server_call/3.

И, на­конец, для па­рал­лель­но­го ре­шения ти­пич­ных за­дач в мо­ду­ле rpc оп­ре­де­ле­ны еще две функ­ции: rpc:parallel_eval/1 и rpc:pmap/3. Функ­ция rpc:parallel_eval/1 по­зво­ля­ет па­рал­лель­но вы­чис­лить зна­чения функ­ций на уз­лах, со­единенных с дан­ным уз­лом, и воз­вра­ща­ет ре­зуль­тат это­го вы­чис­ления в том же по­ряд­ке, в ка­ком шли ис­ход­ные функ­ции. Функ­ция rpc:pmap/3 яв­ля­ет­ся вер­си­ей функ­ции lists:map/2, ра­бо­таю­щей па­рал­лель­но.

Зай­мем­ся ра­бо­той с уз­ла­ми. В про­шлом но­ме­ре мы го­во­ри­ли о мо­ду­ле global, ко­то­рый со­дер­жит функ­ции для ра­бо­ты с гло­баль­ны­ми за­ре­ги­ст­ри­ро­ван­ны­ми име­на­ми про­цес­сов (это функ­ции global:register_name/2,3, global:re_register_name/2,3, global:registered_names/0, global:unregister_name/1, global:whereis_name/1 и global:send/2). По­ми­мо это­го, дан­ный мо­дуль со­дер­жит функ­цио­наль­ность для ра­бо­ты с гло­баль­ны­ми бло­ки­ров­ка­ми. Гло­баль­ная бло­ки­ров­ка – это бло­ки­ров­ка на ка­кой-ли­бо ре­сурс (за­дан­ный иден­ти­фи­ка­то­ром) ли­бо на уровне уз­ла, ли­бо на уровне всех уз­лов, со­единен­ных друг с дру­гом. Для управ­ления бло­ки­ров­ка­ми ис­поль­зу­ет­ся спе­ци­аль­ный иден­ти­фи­ка­тор, ко­то­рый име­ет вид {ResourceId, LockRequesterId}, где ResourceId – неко­то­рый иден­ти­фи­ка­тор ре­сур­са, LockRequesterId – иден­ти­фи­ка­тор то­го, кто за­пра­ши­ва­ет. И ResourceId, и LockRequesterId мо­гут быть лю­бы­ми объ­ек­та­ми язы­ка Erlang; иден­ти­фи­ка­тор за­пра­ши­вае­мой сто­ро­ны не обя­за­тель­но дол­жен быть иден­ти­фи­ка­то­ром или за­ре­ги­ст­ри­ро­ван­ным именем про­цес­са. Мы по­го­во­рим бо­лее под­роб­но о гло­баль­ных бло­ки­ров­ках на сле­дую­щем уроке, а сей­час про­сто рас­смот­рим пред­на­зна­че­нные для это­го функ­ции. Что­бы уста­но­вить бло­ки­ров­ку, ис­поль­зу­ет­ся се­мей­ст­во функ­ций global:set_lock/1,2,3. Что­бы снять бло­ки­ров­ку, ис­поль­зу­ет­ся се­мей­ст­во функ­ций global:del_lock/1,2,3. На­конец, у нас есть воз­мож­ность уста­но­вить бло­ки­ров­ку и по­сле успеш­но­го уста­нов­ления вы­чис­лить зна­чение неко­то­рой за­дан­ной функ­ции: для это­го оп­ре­де­ле­но се­мей­ст­во функ­ций global:trans/2,3,4. Ес­ли при уста­нов­лении бло­ки­ров­ки ока­жет­ся, что бло­ки­ров­ка на за­пра­ши­вае­мый ре­сурс уже уста­нов­ле­на кем-то еще, то про­цесс, во вре­мя вы­полнения ко­то­ро­го мы уста­нав­ли­ва­ем бло­ки­ров­ку, пе­рей­дет в со­стояние ожи­дания. Это со­стояние за­кон­чит­ся, как толь­ко бло­ки­ров­ка на за­пра­ши­вае­мый ре­сурс бу­дет сня­та дру­гим про­цес­сом и уста­нов­ле­на на­шим про­цес­сом. А ес­ли уста­нов­ление бло­ки­ров­ки на ре­сурс ожи­да­ют несколь­ко про­цес­сов, то бло­ки­ров­ку уста­но­вит лю­бой из них (мы не мо­жем по­ла­гать­ся на ка­кой-ли­бо по­ря­док уста­нов­ления бло­ки­ров­ки ожи­даю­щи­ми про­цес­са­ми). Со­стояние ожи­дания мо­жет так­же за­кон­чить­ся, ес­ли бу­дет пре­вы­ше­но пре­дель­ное вре­мя ожи­дания, пе­ре­да­вае­мое в ка­че­­ст­ве па­ра­мет­ра (на са­мом де­ле пе­ре­да­ет­ся не пре­дель­ное вре­мя ожи­дания, а ко­ли­че­­ст­во по­пы­ток уста­нов­ления бло­ки­ров­ки), ес­ли мы не ука­за­ли, что уста­нов­ления бло­ки­ров­ки про­цесс дол­жен ожи­дать бес­конеч­но дол­го.

По­след­няя наша сегодняшняя те­ма – это соз­дание и ра­бо­та с вспо­мо­га­тель­ны­ми до­черними уз­ла­ми [slave nodes]. Пусть для соз­дания рас­пре­де­лен­ной сис­те­мы нам необ­хо­ди­мы несколь­ко уз­лов, рас­по­ло­жен­ных на раз­ных ком­пь­ю­те­рах (хостах). По­нят­но, что раз­верты­вание та­кой ин­фра­струк­ту­ры по­тре­бу­ет неко­то­рых добавоч­ных уси­лий по ее ад­минист­ри­ро­ванию и под­держ­ке. Но ес­ли нас уст­раи­ва­ет си­туа­ция (а в боль­шин­ст­ве слу­ча­ев это так), что вре­мя жизни всех раз­во­ра­чи­вае­мых уз­лов бу­дет за­ви­сеть от вре­мени жизни неко­то­ро­го глав­но­го уз­ла, и весь ввод/вы­вод бу­дет ид­ти че­рез этот глав­ный узел (кон­соль­ный и фай­ло­вый ввод/вы­вод), то мы мо­жем силь­но уп­ро­стить се­бе жизнь.

Для это­го нам доста­точ­но соз­дать толь­ко один глав­ный узел ру­ка­ми, по­сле че­го в ко­де инициа­ли­за­ции (или где-то еще) мы мо­жем соз­да­вать вспо­мо­га­тель­ные до­черние уз­лы на оп­ре­де­лен­ных ком­пь­ю­те­рах (хостах). Это оз­на­ча­ет, что мно­же­ст­во ком­пь­ю­те­ров и уз­лов на них мы за­да­дим при по­мо­щи неко­то­ро­го фай­ла кон­фи­гу­ра­ции. Вся эта функ­цио­наль­ность реа­ли­зо­ва­на в мо­ду­ле slave. Мы мо­жем соз­дать но­вый до­черний вспо­мо­га­тель­ный узел на ком­пь­ю­те­ре (хосте) Host, с именем Name (имя соз­да­вае­мо­го уз­ла бу­дет Name@Host) и па­ра­мет­ра­ми команд­ной стро­ки для за­пуска сре­ды вре­мени вы­полнения Erlang (про­цес­са erl) при по­мо­щи се­мей­ст­ва функ­ций slave:start/1,2,3. Мы мо­жем сде­лать то же са­мое при по­мо­щи се­мей­ст­ва функ­ций slave:start_link/1,2,3, но тогда вре­мя жизни соз­дан­но­го вспо­мо­га­тель­но­го уз­ла бу­дет за­ви­сеть от вы­зы­ваю­ще­го про­цес­са. И, на­конец, мо­жно за­вер­шить ра­бо­ту уз­ла при по­мо­щи функ­ции slave:stop/1. Сле­ду­ет ска­зать, что для соз­дания уз­лов на ком­пь­ю­те­рах (хостах), от­лич­ных от то­го, на ко­то­ром за­пу­щен глав­ный узел, ис­поль­зу­ет­ся ути­ли­та rsh (или при за­пуске глав­но­го уз­ла при по­мо­щи клю­ча –rsh Program за­да­ет­ся аль­тер­на­ти­ва ути­ли­те rsh).

С до­черними вспо­мо­га­тель­ны­ми уз­ла­ми (и мо­ду­лем slave) свя­за­на еще од­на близ­кая те­ма: пу­лы уз­лов. Это пре­до­пре­де­лен­ные на­боры уже соз­дан­ных уз­лов, пред­на­зна­чен­ные для об­слу­жи­вания за­про­сов кли­ен­тов на вы­полнение той или иной функ­ции; при этом вы­бо­ром уз­ла и соз­данием про­цес­са для вы­полнения занима­ет­ся сам пул уз­лов. В ка­че­­ст­ве та­ко­го уз­ла вы­би­ра­ет­ся узел с наи­мень­шей за­груз­кой. Соз­дать пул уз­лов с именем Name мож­но при по­мо­щи се­мей­ст­ва функ­ций pool:start/1,2. Узел, на ко­то­ром про­ис­хо­дит вы­зов од­ной из функ­ций pool:start/1,2, ста­но­вит­ся глав­ным уз­лом пу­ла. Вспо­мо­га­тель­ные уз­лы пу­ла соз­да­ют­ся при по­мо­щи од­ной из функ­ций slave:start/2,3; файл .hosts.erlang оп­ре­де­ля­ет хосты, где бу­дут соз­да­ны вспо­мо­га­тель­ные уз­лы (на ка­ж­дом хосте из это­го фай­ла по од­но­му уз­лу). Ес­ли файл .hosts.erlang в сис­те­ме от­сут­ст­ву­ет, соз­дание пу­ла уз­лов за­вер­шит­ся с ошиб­кой. Мы мо­жем при­сое­динить уже соз­дан­ный узел к пу­лу при по­мо­щи функ­ции pool:attach/1. При по­мо­щи функ­ции pool:stop/0 мы мо­жем за­вер­шить ра­бо­ту пу­ла (и «убить» все до­черние уз­лы). При по­мо­щи функ­ции pool:get_nodes/0 мы по­лу­чим спи­сок всех уз­лов, со­став­ляю­щих пул, а при по­мо­щи функ­ции pool:get_node/0 – наи­менее за­гру­жен­ный узел из пу­ла.

И, на­конец, при по­мо­щи функ­ций pool:pspawn/3 и pool:pspawn_link/3 соз­да­ет­ся про­цесс для вы­полнения функ­ции Module:Func с ар­гу­мен­та­ми Args; при этом вы­бо­ром уз­ла и соз­данием про­цес­са занима­ет­ся пул по­то­ков. Функ­ции pool:pspawn/3 и pool:pspawn_link/3 воз­вра­ща­ют управ­ление сра­зу по­сле соз­дания про­цес­са; при этом они воз­вра­ща­ют иден­ти­фи­ка­тор соз­дан­но­го про­цес­са. Это оз­на­ча­ет, что ес­ли надо по­лу­чить зна­чение функ­ции, об этом следует по­за­бо­тить­ся са­мо­му; на­при­мер, в ка­че­­ст­ве Module:Func пе­ре­да­вать неко­то­рую функ­цию-оберт­ку, ко­то­рая внут­ри вы­пол­ня­ет ин­те­ре­сую­щую нас функ­цию и по­сы­ла­ет нам ее зна­чение че­рез ме­ханизм со­об­щений.

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

Хра­не­ние со­стоя­ния в цик­ле об­ра­бот­ки со­об­ще­ний.

Рас­смот­рим при­мер неко­то­рой ги­по­те­ти­че­­ской функ­ции со­об­щений, де­мон­ст­ри­руующий, как хранить со­стояние в цик­ле об­ра­бот­ки со­об­щений:

message_handler(State) ->

receive

{m1, From} ->

ReturnValue = some_func1(State),

From ! {r1, ReturnValue},

message_handler(State);

{m2, Data} ->

NewState = some_func2(State, Data),

message_handler(NewState);

_Other -> message_handler(State)

end.

При­ве­ден­ный при­мер со­дер­жит три об­ра­бот­чи­ка со­об­щений: для со­об­щений {m1, From}, для со­об­щений {m2, Data} и для всех осталь­ных со­об­щений.

При об­ра­бот­ке со­об­щения {m1, From} мы вы­чис­ля­ем неко­то­рое зна­чение с ис­поль­зо­ванием те­ку­ще­го со­стояния, воз­вра­ща­ем его инициа­то­ру за­про­са From, и вы­зы­ва­ем ре­кур­сив­но са­ми се­бя, пе­ре­да­вая те­ку­щее со­стояние State в ка­че­­ст­ве па­ра­мет­ра. Вид­но, что в дан­ном об­ра­бот­чи­ке со­стояние не ме­ня­ет­ся.

При об­ра­бот­ке со­об­щения {m2, Data} мы вы­чис­ля­ем но­вое со­стояние NewState с ис­поль­зо­ванием ста­ро­го со­стояния State и неко­то­рых дан­ных Data, по­сле че­го ре­кур­сив­но вы­зы­ва­ем са­ми се­бя, пе­ре­да­вая но­вое со­стояние NewState в ка­че­­ст­ве па­ра­мет­ра.

По­следний об­ра­бот­чик де­мон­ст­ри­ру­ет нам, как при по­мо­щи ме­ханиз­ма со­от­вет­ст­вие шаб­ло­ну (pattern matching) мы мо­жем об­ра­ба­ты­вать (в на­шем слу­чае про­сто из­вле­кать из оче­ре­ди со­об­щений) все осталь­ные со­об­щения. В этом об­ра­бот­чи­ке мы с со­об­щением ниче­го не де­ла­ем, а про­сто вы­зы­ва­ем ре­кур­сив­но са­ми се­бя, пе­ре­да­вая те­ку­щее со­стояние State в ка­че­­ст­ве па­ра­мет­ра.

И, на­конец, при соз­дании про­цес­са мы за­да­ем на­чаль­ное со­стояние для об­ра­бот­чи­ка со­об­щений сле­дую­щим об­ра­зом: spawn(fun() -> message_hadler(InitState) end).

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