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

LXF160:Erlang

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


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

Содержание

Erlang: Рас­пре­де­лен­ные сис­те­мы

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

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

В двух про­шлых но­ме­рах мы го­во­ри­ли о мно­го­за­дач­но­сти и об устой­чи­во­сти к ошиб­кам в язы­ке Erlang. Се­го­дня мы про­дол­жим раз­го­вор о сред­ст­вах по­строения про­мыш­лен­ных при­ло­жений и по­го­во­рим о та­ком важ­ном по­ня­тии, как рас­пре­де­лен­ные сис­те­мы.

Ос­нов­ны­ми эле­мен­та­ми для по­строения рас­пре­де­лен­ных сис­тем на язы­ке Erlang яв­ля­ют­ся уз­лы. Узел – это все­го лишь име­но­ван­ный эк­зем­п­ляр сре­ды вы­полнения Erlang. Что­бы сде­лать эк­зем­п­ляр сре­ды вы­полнения уз­лом, доста­точ­но при ее за­пуске за­дать один из па­ра­мет­ров –name или –sname и имя, ко­то­рое узел по­лу­чит. Па­ра­метр –sname за­да­ет ко­рот­кое имя; это имя мо­жет ис­поль­зо­вать­ся, ес­ли соз­да­вае­мые уз­лы на­хо­дят­ся ли­бо на од­ном ком­пь­ю­те­ре, ли­бо на раз­ных ком­пь­ю­те­рах в од­ной под­се­ти. Кро­ме то­го, ес­ли сер­вис DNS не досту­пен для соз­да­вае­мо­го уз­ла, то соз­да­вать узел мож­но толь­ко с па­ра­мет­ром –sname. Па­ра­метр –name по­зво­ля­ет за­дать длин­ное имя; это имя мо­жет ис­поль­зо­вать­ся во всех тех же слу­ча­ях, что и ко­рот­кое, а так­же в слу­ча­ях, когда уз­лы рас­по­ла­га­ют­ся на раз­ных ком­пь­ю­те­рах в раз­ных под­се­тях.

Пусть имя ком­пь­ю­те­ра яв­ля­ет­ся пол­но­стью оп­ре­де­лен­ным до­мен­ным именем – на­при­мер, stdstring.example.com. В этом слу­чае, ес­ли мы соз­да­ем узел с клю­чом –sname test, имя уз­ла бу­дет test@stdstring, а с клю­чом –name test – test@stdstring.example.com. Те­перь пред­по­ло­жим, что имя ком­пь­ю­те­ра яв­ля­ет­ся обыч­ным именем (а не пол­но­стью оп­ре­де­лен­ным до­мен­ным именем), на­при­мер, stdstring. Тогда, ес­ли мы соз­да­ем узел с клю­чом –sname test, имя уз­ла бу­дет test@stdstring. Ес­ли же по­пы­тать­ся соз­дать узел с клю­чом –name test, мы по­лу­чим ошиб­ку при соз­дании уз­ла (при за­пуске эк­зем­п­ля­ра сре­ды вы­полнения Erlang). Ре­шение в этом слу­чае доста­точ­но про­стое: при соз­дании уз­ла с по­мо­щью клю­ча –name сле­ду­ет ука­зать пол­ное имя уз­ла. Ес­ли, на­при­мер, соз­да­вать узел мы бу­дем с клю­чом –name test@stdstring.cpp, то ар­гу­мент клю­ча test@stdstring.cpp и бу­дет именем соз­дан­но­го уз­ла. В дальней­шем, когда мы бу­дем ис­поль­зо­вать имя уз­ла, его сле­ду­ет за­да­вать как атом. Ес­ли ато­мы в сво­ем со­ста­ве со­дер­жат сим­во­лы, от­лич­ные от строч­ных ла­тин­ских букв и сим­во­ла '_', то те­ло ато­ма долж­но быть за­клю­че­но в оди­нар­ные ка­выч­ки. Та­к, на­при­мер, для уз­ла test@stdstring.cpp его имя мы бу­дем за­да­вать сле­дую­щим об­ра­зом: 'test@stdstring.cpp’.

При на­зна­чении уз­лам имен сле­ду­ет учи­ты­вать сле­дую­щее со­об­ра­жение: уз­лы с раз­ны­ми ти­па­ми имен (т. е. с длин­ны­ми и ко­рот­ки­ми) не мо­гут взаи­мо­дей­ст­во­вать друг с дру­гом че­рез стан­дарт­ный ме­ханизм об­ме­на со­об­щения­ми. Ес­ли та­кое взаи­мо­дей­ст­вие необ­хо­ди­мо, за­да­вай­те соз­да­вае­мым уз­лам име­на од­но­го ти­па. От­ме­тим так­же, что ес­ли эк­зем­п­ляр сре­ды вы­полнения Erlang не яв­ля­ет­ся уз­лом, то имя у та­ко­го эк­зем­п­ля­ра все рав­но есть: это атом nonode@nohost.

По­ми­мо имени, при соз­дании уз­ла необ­хо­ди­мо за­да­вать так­же и неко­то­рое «ма­ги­че­­ское» зна­чение, на­зы­вае­мое magic cookie. Дан­ное зна­чение яв­ля­ет­ся ато­мом и ис­поль­зу­ет­ся для про­цес­са ау­тен­ти­фи­ка­ции уз­лов в мо­мент уста­нов­ления со­единения ме­ж­ду ними. При этом про­ис­хо­дит сле­дую­щее: когда один узел пы­та­ет­ся уста­но­вить со­единение с дру­гим уз­лом, сре­да вы­полнения Erlang сравнива­ет «ма­ги­че­­ские» зна­чения этих двух уз­лов; ес­ли они рав­ны, то связь ме­ж­ду уз­ла­ми уста­нав­ли­ва­ет­ся, ес­ли же нет, связь ме­ж­ду уз­ла­ми уста­нов­ле­на не бу­дет. По­сле уста­нов­ления со­единения ме­ж­ду уз­ла­ми их «ма­ги­че­­ские» зна­чения бо­лее не ис­поль­зу­ют­ся. А зна­чит, по­сле уста­нов­ления со­единения с ка­ким-ли­бо уз­лом с одним «ма­ги­че­­ским» зна­чением, мы, из­менив «ма­ги­че­­ское» зна­чение на те­ку­щем уз­ле, мо­жем уста­но­вить со­единение с дру­гим уз­лом с дру­гим «ма­ги­че­­ским» зна­чением. Пусть, на­при­мер, у нас есть два уз­ла – node1@comp1 и node2@comp2, для ко­то­рых «ма­ги­че­­ские» зна­чения уста­нов­ле­ны в A и B со­от­вет­ст­вен­но. Пе­ред тем, как уста­но­вить со­единение с уз­лом node1@comp1, мы уста­нав­ли­ва­ем «ма­ги­че­­ское» зна­чение на те­ку­щем уз­ле в A, по­сле че­го, же­лая уста­но­вить со­единение с уз­лом node2@comp2, мы уста­нав­ли­ва­ем «ма­ги­че­­ское» зна­чение на те­ку­щем уз­ле в B. «Ма­ги­че­­ское» зна­чение на уз­ле мож­но уста­но­вить тре­мя спо­со­ба­ми, при­чем два из них по­зво­ля­ют уста­но­вить «ма­ги­че­­ское» зна­чение уз­ла толь­ко при его соз­дании. Са­мый про­стой спо­соб – ниче­го не де­лать; при этом сре­да вы­полнения Erlang (при за­пуске ее как уз­ла) са­ма про­чи­та­ет зна­чение из фай­ла $HOME/.erlang.cookie (а ес­ли та­ко­го фай­ла нет, то сре­да вы­полнения Erlang его соз­даст). Что­бы при та­ком спо­со­бе несколь­ко уз­лов, рас­по­ло­жен­ные на раз­ных ком­пь­ю­те­рах, мог­ли уста­но­вить со­единение друг с дру­гом, на всех та­ких ком­пь­ю­те­рах со­дер­жи­мое фай­ла $HOME/.erlang.cookie долж­но быть оди­на­ко­вым. Ес­ли же мы соз­да­ем несколь­ко уз­лов на од­ном ком­пь­ю­те­ре, то нам бес­по­ко­ить­ся не о чем: у всех уз­лов, соз­да­вае­мых та­ким спо­со­бом, бу­дет од­но и то же «ма­ги­че­­ское» зна­чение. Дру­гой спо­соб, по­зво­ляю­щий за­дать «ма­ги­че­­ское» зна­чение при соз­дании уз­ла – ис­поль­зо­вать оп­цию команд­ной стро­ки –setcookie и же­лае­мое «ма­ги­че­­ское» зна­чение. На­при­мер, ес­ли при соз­дании уз­ла с ко­рот­ким именем test (на ком­пь­ю­те­ре с именем stdstring) мы хо­тим, что­бы «ма­ги­че­­ское» зна­чение бы­ло some_value, то сде­лать это мы мо­жем сле­дую­щим об­ра­зом: erl –sname test –setcookie some_value. И на­конец, по­следний спо­соб уста­но­вить «ма­ги­че­­ское» зна­чение – это ис­поль­зо­вать функ­цию erlang:set_cookie(Node, Cookie) мо­ду­ля erlang. Она по­зво­ля­ет уста­но­вить «ма­ги­че­­ское» зна­чение Cookie на уз­ле Node. На­при­мер, что­бы уста­но­вить «ма­ги­че­­ское» зна­чение SomeValue на уз­ле node1@stdstring, мы де­ла­ем сле­дую­щий вы­зов: erlang:set_cookie('node1@stdstring’, ‘SomeValue’). Же­лая по­лу­чить уста­нов­лен­ное «ма­ги­че­­ское» зна­чение на те­ку­щем уз­ле, мы долж­ны ис­поль­зо­вать функ­цию erlang:get_cookie/0 мо­ду­ля erlang. Вполне по­нят­на при­чи­на, по ко­то­рой мы мо­жем по­лу­чить уста­нов­лен­ное «ма­ги­че­­ское» зна­чение толь­ко на те­ку­щем уз­ле; ес­ли бы мы мог­ли по­лу­чить его на лю­бом уз­ле, это сво­ди­ло бы на нет сис­те­му ау­тен­ти­фи­ка­ции ме­ж­ду уз­ла­ми.

Про­ве­дем несколь­ко экс­пе­ри­мен­тов по уста­нов­лению со­единения ме­ж­ду уз­ла­ми. Для это­го нам по­тре­бу­ет­ся функ­ция (BIF), о ко­то­рой мы еще не го­во­ри­ли: nodes/1. Она по­зво­ля­ет по­лу­чить спи­сок имен уз­лов оп­ре­де­лен­но­го ти­па, так или ина­че свя­зан­ных с те­ку­щим уз­лом. Ес­ли в ка­че­­ст­ве ар­гу­мен­та пе­ре­дать атом visible, мы по­лу­чим спи­сок ви­ди­мых уз­лов, т. е. уз­лов, уста­но­вив­ших обыч­ное со­единение с те­ку­щим уз­лом; ес­ли атом hidden, мы по­лу­чим спи­сок неви­ди­мых уз­лов, т. е. уз­лов, уста­но­вив­ших «неви­ди­мое» со­единение с те­ку­щим уз­лом (мы по­го­во­рим об этом чуть поз­же); атом connected – спи­сок всех уз­лов, уста­но­вив­ших со­единение с те­ку­щим уз­лом; атом this – спи­сок, со­дер­жа­щий толь­ко имя те­ку­ще­го уз­ла. И, на­конец, ес­ли в ка­че­­ст­ве ар­гу­мен­та пе­ре­дать атом known, мы по­лу­чим спи­сок всех уз­лов, из­вест­ных те­ку­ще­му уз­лу, т. е. тех, ко­то­рые когда-ли­бо уста­нав­ли­ва­ли с ним со­единение (так­же этот спи­сок бу­дет со­дер­жать и имя те­ку­ще­го уз­ла). Кро­ме то­го, мы мо­жем пе­ре­дать при­ве­ден­ные вы­ше ато­мы в ка­че­­ст­ве спи­ска ар­гу­мен­тов; тогда мы по­лу­чим спи­сок имен уз­лов, ко­то­рый яв­ля­ет­ся объ­е­динением спи­сков имен уз­лов, по­лу­чен­ных для ка­ж­до­го ар­гу­мен­та. Сле­ду­ет так­же ска­зать, что су­ще­ст­ву­ет функ­ция (BIF) nodes/0, яв­ляю­щая­ся ана­ло­гом вы­зо­ва nodes(visible).

По­сле это­го неболь­шо­го всту­п­ления пе­рей­дем к собственно экс­пе­ри­мен­там. Для на­ча­ла соз­да­дим три уз­ла с ко­рот­ки­ми име­на­ми (на ком­пь­ю­те­ре stdstring): n1@stdstring, n2@stdstring и n3@stdstring. Уз­лы в рас­пре­де­лен­ной сре­де Erlang яв­ля­ют­ся сла­бо­свя­зан­ны­ми; это оз­на­ча­ет, что по­ка к уз­лу не бы­ло об­ра­щения со сто­ро­ны дру­го­го уз­ла, со­единение ме­ж­ду эти­ми уз­ла­ми от­сут­ст­ву­ет. Ес­ли мы на уз­ле n1@stdstring вве­дем nodes(visible), то по­лу­чим пустой спи­сок; это оз­на­ча­ет, что уз­лы при соз­дании не уста­нав­ли­ва­ют со­единение с уже соз­дан­ны­ми уз­ла­ми. Для уста­нов­ления со­единения ме­ж­ду уда­лен­ным и те­ку­щим уз­ла­ми необ­хо­ди­мо ис­поль­зо­вать имя уда­лен­но­го уз­ла в од­ной из функ­ций, об­ра­щаю­щих­ся к за­дан­но­му уз­лу. К та­ким функ­ци­ям от­но­сят­ся, на­при­мер, се­мей­ст­во функ­ций spawn/2,4 (о них мы по­го­во­рим чуть поз­же). Про­стей­шей же функ­ци­ей, ко­то­рая уста­нав­ли­ва­ет со­единение ме­ж­ду уда­лен­ным и те­ку­щим уз­ла­ми, яв­ля­ет­ся функ­ция net_adm:ping/1 мо­ду­ля net_adm. Эта функ­ция про­ве­ря­ет, досту­пен ли уда­лен­ный узел для уста­нов­ления со­единения (что яс­но из на­звания этой функ­ции); ес­ли да, то со­единение ме­ж­ду уда­лен­ным и те­ку­щим уз­ла­ми уста­нав­ли­ва­ет­ся и воз­вра­ща­ет­ся атом pong, ина­че воз­вра­ща­ет­ся атом pang. Да­вай­те на уз­ле n1@stdstring вве­дем net_adm:ping(‘n2@stdstring’); по­сле это­го на уз­лах n1@stdstring и n2@stdstring вве­дем nodes(visible). На уз­ле n1@stdstring мы по­лу­чим спи­сок [n2@stdstring], а на уз­ле n2@stdstring – спи­сок [n1@stdstring]. Это оз­на­ча­ет, что со­единение ме­ж­ду эти­ми уз­ла­ми бы­ло уста­нов­ле­но. Да­вай­те на уз­ле n1@stdstring вве­дем net_adm:ping(‘n3@stdstring’); по­сле это­го на уз­ле n1@stdstring вве­дем nodes(visible) – и по­лу­чим спи­сок ['n2@stdstring’, ‘n3@stdstring’], что ожи­дае­мо. Те­перь на уз­лах n2@stdstring и n3@stdstring вве­дем nodes(visible); мы по­лу­чим спи­ски ['n1@stdstring’, ‘n3@stdstring’] и ['n1@stdstring’, ‘n2@stdstring’] со­от­вет­ст­вен­но. Этот несколь­ко неожи­дан­ный для нас ре­зуль­тат оз­на­ча­ет, что уста­нов­ление со­единения – про­цесс тран­зи­тив­ный. Т. е. когда узел A уста­нав­ли­ва­ет со­единение с уз­лом B, уже имею­щий уста­нов­лен­ное со­единение с уз­лом C, то узел A по­пы­та­ет­ся уста­но­вить со­единение и с уз­лом C.

Тран­зи­тив­ность про­цес­са уста­нов­ления со­единения яв­ля­ет­ся по­ве­дением по умол­чанию; од­на­ко его мож­но из­менить, соз­да­вая узел с клю­чом –connect_all false. Ес­ли та­кой узел уча­ст­ву­ет в про­цес­се уста­нов­ления со­единения, уста­но­вит­ся толь­ко од­но со­единение: ме­ж­ду инициа­то­ром и яв­но ука­зан­ным уз­лом (ес­ли вдруг «ма­ги­че­­ские» зна­чения у этих уз­лов не сов­па­дут, то ника­ко­го со­единения не бу­дет). Пред­по­ло­жим, что при соз­дании уз­ла n3@stdstring мы ука­за­ли флаг –connect_all false, по­сле че­го на уз­ле n1@stdstring по­сле­до­ва­тель­но сде­ла­ли два вы­зо­ва net_adm:ping(‘n2@stdstring’) и net_adm:ping(‘n3@stdstring’). В этом слу­чае, вы­зов nodes(visible) на уз­ле n1@stdstring вернет ['n2@stdstring’, ‘n3@stdstring’], вы­зов nodes(visible) на уз­ле n2@stdstring вернет ['n1@stdstring’], то же са­мое вернет и вы­зов nodes(visible) на уз­ле n3@stdstring.

По­доб­ное по­ве­дение при уста­нов­лении со­единения (от­сут­ст­вие тран­зи­тив­но­сти) мож­но по­лу­чить, ес­ли при соз­дании уз­ла ука­зать, что это узел дол­жен быть неви­ди­мым. Для это­го при соз­дании уз­ла необ­хо­ди­мо ука­зать ключ –hidden. При уста­нов­лении со­единения ме­ж­ду уз­ла­ми, ес­ли хо­тя бы один из уз­лов яв­ля­ет­ся неви­ди­мым, бу­дет уста­нов­ле­но т. н. неви­ди­мое со­единение. Это оз­на­ча­ет, что в ре­зуль­та­тах вы­зо­ва nodes() или nodes(visible) уз­лы, со­единен­ные с те­ку­щим уз­лом неви­ди­мым со­единением, бу­дут от­сут­ст­во­вать. Что­бы по­лу­чить уз­лы, со­единен­ные с дан­ным уз­лом при по­мо­щи неви­ди­мых со­единений, необ­хо­ди­мо ис­поль­зо­вать вы­зов nodes(hidden); что­бы по­лу­чить все уз­лы, со­единен­ные с дан­ным уз­лом – вы­зов nodes(connected). Для че­го нуж­ны неви­ди­мые уз­лы? Для то­го, что­бы вес­ти про­вер­ку неко­то­рых час­тей сис­те­мы (рас­по­ла­гаю­щих­ся на од­ном или несколь­ких уз­лах), не вли­яя на всю сис­те­му це­ли­ком (не уста­нав­ли­вая со­единения со все­ми осталь­ны­ми уз­ла­ми).

Да­вай­те вернем­ся к на­ше­му при­ме­ру. Пред­по­ло­жим, что при соз­дании уз­ла n3@stdstring мы ука­за­ли флаг –hidden, по­сле че­го на уз­ле n1@stdstring по­сле­до­ва­тель­но сде­ла­ли два вы­зо­ва net_adm:ping(‘n2@stdstring’) и net_adm:ping(‘n3@stdstring’). В этом слу­чае, вы­зов nodes(visible) на уз­ле n1@stdstring вернет ['n2@stdstring’], вы­зов nodes(hidden) вернет ['n3@stdstring’], а вы­зов nodes(connected) вернет ['n2@stdstring’, ‘n3@stdstring’]. На уз­ле n2@stdstring вы­зо­вы nodes(), nodes(visible), nodes(connected) вер­нут ['n1@stdstring’], а вы­зов nodes(hidden) вернет пустой спи­сок. На уз­ле n3@stdstring вы­зо­вы nodes(), nodes(visible) вер­нут пустой спи­сок, вы­зо­вы nodes(hidden), nodes(connected) вер­нут ['n1@stdstring’].

Ра­зо­рвать со­единение ме­ж­ду уз­ла­ми мож­но дву­мя спо­со­ба­ми. Во-пер­вых, мож­но за­вер­шить ра­бо­ту уз­ла сред­ст­ва­ми опе­ра­ци­он­ной сис­те­мы. Во-вто­рых, мож­но ра­зо­рвать со­единение ме­ж­ду те­ку­щим и неко­то­рым дру­гим уз­ла­ми при по­мо­щи функ­ции (BIF) disconnect_node/1, ко­то­рая в ка­че­­ст­ве ар­гу­мен­та принима­ет имя уз­ла на дру­гой сто­роне уста­нов­лен­но­го со­единения. В кон­тек­сте раз­го­во­ра о раз­ры­ве со­единений, по­го­во­рим так­же об из­вест­ных уз­лах (уз­лах, ко­то­рые мы мо­жем по­лу­чить вы­зо­вом nodes(known)). К из­вест­ным от­но­си­тель­но те­ку­ще­го уз­ла от­но­сят­ся уз­лы, когда-ли­бо уста­нав­ли­вав­шие со­единение с те­ку­щим уз­лом. Это оз­на­ча­ет, что, ес­ли со­единение ме­ж­ду те­ку­щим и ка­ким-ли­бо дру­гим уз­лом уже ра­зо­рва­но, то это­го уз­ла в спи­ске, ко­то­рый воз­вра­ща­ет вы­зов nodes(connected), уже не бу­дет, а в спи­ске, ко­то­рый воз­вра­ща­ет вы­зов nodes(known), он бу­дет при­сут­ст­во­вать. Ес­ли же со­единение ме­ж­ду те­ку­щим и ка­ким-ли­бо дру­гим уз­лом не ра­зо­рва­но, то этот узел бу­дет при­сут­ст­во­вать как в спи­ске, воз­вра­щае­мом вы­зо­вом nodes(known), так и в спи­ске, воз­вра­щае­мом вы­зо­вом nodes(connected).

Опять вернем­ся к при­ме­ру. Пусть мы соз­да­ли три уз­ла n1@stdstring, n2@stdstring и n3@stdstring обыч­ным спо­со­бом (без вся­ких до­полнитель­ных фла­гов). Затем на уз­ле n1@stdstring мы по­сле­до­ва­тель­но сде­ла­ли три вы­зо­ва net_adm:ping(‘n2@stdstring’), net_adm:ping(‘n3@stdstring’), disconnect_node('n3@stdstring’). Вы­зов функ­ции nodes(connected) на уз­ле n1@stdstring вернет спи­сок ['n2@stdstring’], а вы­зов функ­ции nodes(known) вернет спи­сок ['n1@stdstring’, ‘n2@stdstring’, ‘n3@stdstring’] (мы помним, что спи­сок из­вест­ных уз­лов со­дер­жит так­же и имя те­ку­ще­го уз­ла). На уз­ле n3@stdstring все бу­дет ана­ло­гич­но: вы­зов функ­ции nodes(connected) вернет спи­сок ['n2@stdstring’], а вы­зов функ­ции nodes(known) вернет спи­сок ['n1@stdstring’, ‘n2@stdstring’, ‘n3@stdstring’]. И, на­конец, на уз­ле n2@stdstring (с ко­то­рым никто из уз­лов со­единение не раз­ры­вал) вы­зов функ­ции nodes(connected) вернет спи­сок ['n1@stdstring’, ‘n3@stdstring’], а вы­зов функ­ции nodes(known) вернет спи­сок ['n1@stdstring’, ‘n2@stdstring’, ‘n3@stdstring’].

Мы доста­точ­но мно­го го­во­ри­ли об уз­лах, но глав­ный во­прос по­ка остал­ся за ка­дром: как ис­поль­зо­вать уз­лы для соз­дания рас­пре­де­лен­ных при­ло­жений. От­вет на этот во­прос доста­точ­но прост: имя уз­ла нам нуж­но толь­ко для соз­дания про­цес­са од­ной из функ­ций из се­мейств spawn/2,4, spawn_link/2,4, spawn_opt/3,5, ко­то­рые принима­ют имя уз­ла в ка­че­­ст­ве пер­во­го па­ра­мет­ра. И все. Эти функ­ции воз­вра­ща­ют иден­ти­фи­ка­тор соз­дан­но­го про­цес­са, для ра­бо­ты с ко­то­рым имя уз­ла не нуж­но. Для взаи­мо­дей­ст­вия про­цес­сов опять же ис­поль­зу­ют­ся их иден­ти­фи­ка­то­ры, и знать уз­лы, на ко­то­рых эти про­цес­сы рас­по­ла­га­ют­ся, не нуж­но. Та­ким об­ра­зом, вид­но, что при на­пи­сании мно­го­за­дач­ных при­ло­жений, вы­пол­няю­щих­ся в од­ном эк­зем­п­ля­ре сре­ды вы­полнения Erlang и рас­пре­де­лен­ных при­ло­жений, все от­ли­чие бу­дет толь­ко при соз­дании про­цес­сов. Это за­ме­ча­тель­ный факт, и мы к нему еще вернем­ся при реа­ли­за­ции прак­ти­че­­ских за­дач.

Рас­смот­рим те­перь ба­зо­вый на­бор функ­ций, пред­на­зна­чен­ный для ра­бо­ты с уз­ла­ми (боль­шую часть из этих функ­ций мы уже ви­де­ли и ис­поль­зо­ва­ли). Для управ­ления «ма­ги­че­­ски­­ми» зна­чения­ми у нас есть две функ­ции: erlang:get_cookie/0 и erlang:set_cookie/2. Пер­вая воз­вра­ща­ет «ма­ги­че­­ское» зна­чение те­ку­ще­го уз­ла, вто­рая по­зво­ля­ет за­дать «ма­ги­че­­ское» зна­чение про­из­воль­но­му уз­лу. Что­бы ра­зо­рвать со­единение ме­ж­ду те­ку­щим и за­дан­ным уз­ла­ми, ис­поль­зу­ет­ся функ­ция (BIF) disconnect_node/1. Функ­ция (BIF) is_alive/0 по­зво­ля­ет оп­ре­де­лить, мо­жет ли те­ку­щий узел (а точнее, эк­зем­п­ляр сре­ды вре­мени вы­полнения Erlang) быть ча­стью рас­пре­де­лен­ной сис­те­мы уз­лов. Функ­ции monitor_node/2 и erlang:monitor_node/3 по­зво­ля­ют вклю­чать, вы­клю­чать и уста­нав­ли­вать оп­ции монито­рин­га жизнен­но­го цик­ла уз­ла. Ес­ли узел пре­кра­тит свою ра­бо­ту (или ес­ли узел не су­ще­ст­ву­ет), те­ку­ще­му уз­лу бу­дет по­сла­но со­об­щение {nodedown, Node}, где Node – имя уз­ла, за ко­то­рым осу­ще­ст­в­лял­ся монито­ринг. Функ­ция node/0 по­зво­ля­ет по­лу­чить имя те­ку­ще­го уз­ла; функ­ция node/1 – имя уз­ла, на ко­то­ром рас­по­ло­жен объ­ект (про­цесс, порт, ссыл­ка). Функ­ция nodes/1 воз­вра­ща­ет спи­сок уз­лов, ко­то­рые так или ина­че свя­за­ны с те­ку­щим уз­лом и удов­ле­тво­ря­ют оп­ре­де­лен­ным кри­те­ри­ям (пе­ре­да­вае­мым в ка­че­­ст­ве ар­гу­мен­та); функ­ция nodes/0 яв­ля­ет­ся ана­ло­гом вы­зо­ва nodes(visible). И, на­конец, се­мей­ст­ва функ­ций spawn/2,4, spawn_link/2,4, spawn_opt/3,5 по­зво­ля­ют соз­дать про­цесс на уз­ле, имя ко­то­ро­го идет пер­вым ар­гу­мен­том этих функ­ций.

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

Имен­но для это­го и нуж­на ре­ги­ст­ра­ция имен про­цес­сов. Мы мо­жем за­ре­ги­ст­ри­ро­вать неко­то­рое имя (ко­то­рое яв­ля­ет­ся ато­мом) и свя­зать его с оп­ре­де­лен­ным про­цес­сом. В дальней­шем, при взаи­мо­дей­ст­вии с этим про­цес­сом, мы мо­жем ис­поль­зо­вать за­ре­ги­ст­ри­ро­ван­ное имя вме­сто иден­ти­фи­ка­то­ра про­цес­са. Для ре­ги­ст­ра­ции имени про­цес­са слу­жит функ­ция (BIF) register(RegName, Pid), где RegName – ре­ги­ст­ри­руе­мое имя про­цес­са, Pid – иден­ти­фи­ка­тор со­от­вет­ст­вую­ще­го про­цес­са. По­ми­мо ре­ги­ст­ра­ции имени про­цес­са, мы мо­жем эту ре­ги­ст­ра­цию уб­рать (при по­мо­щи функ­ции unregister/1), по­лу­чить спи­сок всех за­ре­ги­ст­ри­ро­ван­ных имен (при по­мо­щи функ­ции registered/0) и по­лу­чить по за­ре­ги­ст­ри­ро­ван­но­му имени про­цес­са его иден­ти­фи­ка­тор (при по­мо­щи функ­ции whereis/1). У имен про­цес­сов, ре­ги­ст­ри­руе­мых по­доб­ным об­ра­зом, есть толь­ко один, но су­ще­ст­вен­ный ми­нус: они ре­ги­ст­ри­ру­ют­ся толь­ко на од­ном оп­ре­де­лен­ном уз­ле (т. е. на двух раз­ных уз­лах мо­гут оказаться два про­цес­са с оди­на­ко­вы­ми за­ре­ги­ст­ри­ро­ван­ны­ми име­на­ми). Этот недоста­ток мож­но обой­ти и об­ра­щать­ся к про­цес­су на уз­ле Node по его за­ре­ги­ст­ри­ро­ван­но­му имени Name при по­мо­щи сле­дую­ще­го син­так­си­са: {Name, Node}.

Для при­ме­ра соз­да­дим про­стой мо­дуль (с именем simple_message_handler), ко­то­рый со­дер­жит толь­ко од­ну экс­пор­ти­руе­мую функ­цию message_loop/0. Эта функ­ция пред­став­ля­ет собой про­сто бес­конеч­ный цикл об­ра­бот­ки со­об­щений, для ка­ж­до­го по­лу­чен­но­го со­об­щения вы­во­дящий его на эк­ран. Вот те­ло этой функ­ции:

message_loop() ->

receive

Message -> io:format(«Message: ~p~n», [Message]),

message_loop()

end.

От­ком­пи­ли­ру­ем этот мо­дуль и соз­да­дим два уз­ла: n1@stdstring и n2@stdstring. На уз­ле n1@stdstring соз­да­дим про­цесс, ра­бо­чей функ­ци­ей ко­то­ро­го бу­дет функ­ция simple_message_handler:message_loop/0 (бес­конеч­ный цикл об­ра­бот­ки со­об­щений): ProcessPid = spawn(fun simple_message_handler: message_loop/0). По­сле это­го за­ре­ги­ст­ри­ру­ем имя для вновь соз­дан­но­го про­цес­са: register(simple_service, ProcessPid). Те­перь мы (все еще на­хо­дясь на уз­ле n1@stdstring) мо­жем по­слать со­об­щение вновь соз­дан­но­му про­цес­су при по­мо­щи за­ре­ги­ст­ри­ро­ван­но­го имени: simple_service!{simple_message, message_data}. В ре­зуль­та­те мы уви­дим сле­дую­щий вы­вод на кон­соль (на уз­ле n1@stdstring):

Message: {simple_message, message_data}.

Это оз­на­ча­ет, что со­об­щение соз­дан­ным на­ми про­цес­сом по­лу­че­но и об­ра­бо­та­но. Те­перь по­про­бу­ем на уз­ле n2@stdstring об­ра­тить­ся к соз­дан­но­му про­цес­су по за­ре­ги­ст­ри­ро­ван­но­му имени: simple_service!{simple_message, message_data}. В ре­зуль­та­те мы по­лу­чим ошиб­ку вре­мени вы­полнения, т. к. за­ре­ги­ст­ри­ро­ван­ные име­на яв­ля­ют­ся локаль­ны­ми от­но­си­тель­но уз­ла. По­про­бу­ем об­ра­тить­ся к соз­дан­но­му про­цес­су по за­ре­ги­ст­ри­ро­ван­но­му имени еще раз, но ис­поль­зуя спе­ци­аль­ный син­так­сис: {simple_service, ‘n1@stdstring’}!{simple_message, message_data}. В ре­зуль­та­те на уз­ле n2@stdstring вы­полнение это­го вы­ра­жения бу­дет ус­пешно, а в кон­со­ли на уз­ле n1@stdstring мы уви­дим сле­дую­щий вы­вод:

Message: {simple_message, message_data}.

Как мы уви­де­ли, несмот­ря на то, что ре­ги­ст­ра­ция имен локаль­на для ка­ж­до­го уз­ла, мы мо­жем ис­поль­зо­вать за­ре­ги­ст­ри­ро­ван­ные име­на на лю­бых уз­лах при по­мо­щи спе­ци­аль­но­го син­так­си­са. Но, во-пер­вых, этот син­так­сис не очень удо­бен, т. к. вы­ну­ж­да­ет за­да­вать два имени вме­сто од­но­го, а во-вто­рых (и это доста­точ­но серь­ез­ное ог­раничение), этот син­так­сис рас­кры­ва­ет де­та­ли внут­ренней реа­ли­за­ции сис­те­мы: на ка­ком уз­ле и с ка­ким именем рас­по­ла­га­ет­ся сер­вис. Что­бы пре­одо­леть эти недостат­ки, нам ну­жен ме­ханизм, по­зво­ляю­щий ре­ги­ст­ри­ро­вать име­на гло­баль­но (на уровне ис­поль­зуе­мой се­ти из уз­лов, об­ра­зую­щих при­ло­жение). Для этих це­лей слу­жит мо­дуль global; бо­лее под­роб­но о функ­ци­ях из это­го мо­ду­ля мы по­го­во­рим в сле­дую­щем но­ме­ре, а по­ка ог­раничим­ся лишь гло­баль­ной ре­ги­ст­ра­ци­ей имен. В мо­ду­ле global оп­ре­де­ле­ны сле­дую­щие функ­ции (пол­ный ана­лог функ­ций, ре­ги­ст­ри­рую­щих име­на про­цес­сов локаль­но): global:register_name/2, global:unregister_name/1, global:whereis_name/1. Но при ис­поль­зо­вании этих функ­ций су­ще­ст­ву­ют и неко­то­рые раз­ли­чия: во-пер­вых, узел, же­лаю­щий восполь­зо­вать­ся гло­баль­ным за­ре­ги­ст­ри­ро­ван­ным именем, д. б. из­вес­тен уз­лу, ко­то­рый эту ре­ги­ст­ра­цию осу­ще­ст­вил; во-вто­рых, для от­сыл­ки со­об­щения при по­мо­щи гло­баль­но­го за­ре­ги­ст­ри­ро­ван­но­го имени сле­ду­ет ис­поль­зо­вать функ­цию global:send/2. Обыч­ная функ­ция (BIF) send/2 и опе­ра­тор ! с гло­баль­ны­ми за­ре­ги­ст­ри­ро­ван­ны­ми име­на­ми не ра­бо­та­ют. Наш при­мер в этом слу­чае бу­дет вы­гля­деть так. На уз­ле n1@stdstring мы вво­дим ко­ман­ды

ProcessPid = spawn(fun simple_message_handler: message_loop/0).

global:register_name(simple_service, ProcessPid).

global:send(simple_service, {simple_message, message_data}).

А на уз­ле n2@stdstring мы вво­дим ко­ман­ды

net_adm:ping(‘n1@stdstring’).

global:send(simple_service, {simple_message, message_data}).

В ре­зуль­та­те на уз­ле n1@stdstring на кон­соль два­ж­ды вы­ве­дет­ся сле­дую­щая стро­ка:

Message: {simple_message, message_data}.

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

За­ме­чание о при­ме­рах

В данной ста­тье, когда мы об­су­ж­да­ем при­ме­ры, очень час­то встре­чают­ся инструкции наподобие: на уз­ле с именем NodeName вве­ди­те ко­ман­ду Cmd (на­при­мер – на уз­ле n1@stdstring вве­ди­те ко­ман­ду nodes()). По это­му по­во­ду за­ме­тим сле­дую­щее. Узел яв­ля­ет­ся име­но­ван­ным эк­зем­п­ля­ром сре­ды вы­полнения Erlang. Соз­да­вая узел, мы за­пуска­ем в кон­со­ли сре­ду вы­полнения Erlang с клю­ча­ми. Сре­да вы­полнения Erlang – ин­те­рак­тив­ное кон­соль­ное при­ло­жение; зна­чит, по­сле ее за­пуска мы ра­бо­та­ем уже в кон­со­ли Erlang. А при вво­де ко­ман­ды в кон­со­ли Erlang ка­ж­дую ко­ман­ду необ­хо­ди­мо за­вер­шать сим­во­лом “.”; ко­ман­да от­прав­ля­ет­ся на вы­полнение по­сле на­жа­тия кла­ви­ши Enter. Так что не забывайте ставить точку.

На тем­ной сто­роне си­лы (на Microsoft Windows)

Когда мы соз­да­ем узел (име­но­ван­ный эк­зем­п­ляр сре­ды вы­полнения Erlang) и не ука­зы­ва­ем (при по­мо­щи клю­ча -setcookie) «ма­ги­че­­­ское» зна­чение, оно бе­рет­ся из фай­ла $HOME/.erlang.cookie, а ес­ли та­ко­го фай­ла нет, то сре­да вы­полнения Erlang соз­да­ет его. Пусть мы хо­тим соз­дать несколь­ко уз­лов, рас­по­ла­гаю­щие­ся на раз­ных ком­пь­ю­те­рах, ко­то­рые долж­ны быть со­единены друг с дру­гом, и не хо­тим при этом за­да­вать «ма­ги­че­­­ское» зна­чение при по­мо­щи клю­ча –setcookie или вы­зо­ва erlang:set_cookie/2. Тогда на­до обес­пе­чить, что­бы со­дер­жи­мое фай­ла $HOME/.erlang.cookie на всех ком­пь­ю­те­рах бы­ло оди­на­ко­вым. Как мы зна­ем, язык и сре­да Erlang яв­ля­ют­ся кросс­плат­фор­мен­ны­ми, и воз­мож­на си­туа­ция, что часть уз­лов в та­кой се­ти бу­дут соз­да­вать­ся на ком­пь­ю­те­рах под управ­лением ОС Microsoft Windows. Возника­ет вполне ло­гич­ный во­прос, где в та­ком слу­чае рас­по­ла­га­ет­ся файл, хра­ня­щий «ма­ги­че­­­ское» зна­чение по умол­чанию. В Microsoft Windows этот файл точ­но так же на­зы­ва­ет­ся .erlang.cookie, и рас­по­ла­га­ет­ся он в корне поль­зо­ва­тель­ской ди­рек­то­рии. Най­ти рас­по­ло­жение этой ди­рек­то­рии нам по­мо­гут две пе­ре­мен­ные ок­ру­жения: %HOMEDRIVE % и  %HOMEPATH %. Пер­вая воз­вра­ща­ет бу­к­ву дис­­ка, на ко­то­ром рас­по­ла­га­ет­ся эта ди­рек­то­рия (в Microsoft Windows не су­ще­ст­­ву­ет еди­но­го де­ре­ва ка­та­ло­гов, как в Linux), а вто­рая – путь до поль­зо­ва­тель­ской ди­рек­то­рии (без бу­к­вы дис­­ка). По­это­му пол­ный путь до фай­ла с «ма­ги­че­­­ским» зна­чением по умол­чанию в ОС Microsoft Windows бу­дет %HOMEDRIVE %:\ %HOMEPATH %\.erlang.cookie. Да пре­бу­дет с ва­ми си­ла, и не об­ра­ти­тесь вы на ее тем­ную сто­ро­ну.

Ошиб­ка в пре­ды­ду­щем но­ме­ре

В пре­ды­ду­щем но­ме­ре в за­мет­ках я при­во­дил несколь­ко при­ме­ров ре­шения про­бле­мы с очи­ст­кой ре­сур­сов на язы­ке C. Вто­рой при­мер со­дер­жит до­сад­ную ошиб­ку: вез­де вме­сто сравнения на нера­вен­ст­­во “!=” на­пи­са­но сравнение на ра­вен­ст­­во “==”. Ни­же при­во­дит­ся пра­виль­ный ва­ри­ант это­го при­ме­ра:

int descr1, descr2;

descr1 = open(«file1.dat”, 0_RDWR);

if (-1!= descr1) {

descr2 = open(«file2.dat», 0_RDWR);

if (-1!= descr2) {

some_task(descr1, descr2);

close(descr2);

}

close(descr1);

}|

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