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

LXF159: Устоим перед отказами

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


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

Erlang: Ус­тои­м перед от­ка­зами

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

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

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

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

Рас­смот­рим, ка­кие у нас есть под­хо­ды для ре­шения этих за­дач. Наи­бо­лее про­стой под­ход – ис­поль­зо­вание ко­да ошиб­ки по вы­полнении той или иной опе­ра­ции. Вы­гля­дит это сле­дую­щим об­ра­зом: по­сле вы­полнения той или иной опе­ра­ции мы про­ве­ря­ем спе­ци­аль­ную пе­ре­мен­ную, ко­то­рая слу­жит для хранения ко­да ошиб­ки. Обыч­но та­кие опе­ра­ции яв­ля­ют­ся функ­ция­ми, по воз­вра­щае­мо­му зна­чению ко­то­рых мож­но по­нять, что опе­ра­ция вы­полнилась с ошиб­кой, а уж по ко­ду ошиб­ки мож­но определить, что же кон­крет­но по­шло не так. Для при­ме­ра да­вай­те взглянем на POSIX API. Так, для от­кры­тия фай­ла мы ис­поль­зу­ем функ­цию open. Эта функ­ция от­кры­ва­ет или соз­да­ет файл и воз­вра­ща­ет де­ск­рип­тор это­го фай­ла (це­лое неот­ри­ца­тель­ное чис­ло). Ес­ли что-то пой­дет не так во вре­мя вы­полнения этой опе­ра­ции, функ­ция open вернет -1. В этом слу­чае мы мо­жем об­ра­тить­ся к пе­ре­мен­ной errno, что­бы по­нять, что имен­но по­шло не так (и, воз­мож­но, про­ин­фор­ми­ро­вать об этом поль­зо­ва­те­ля). Из наше­го при­ме­ра вид­но, что ме­тод на са­мом де­ле очень прост. Но у это­го ме­то­да есть ряд серь­ез­ных недостат­ков. Са­мый глав­ный из них за­клю­ча­ет­ся в том, что от­вет­ст­вен­ность за про­вер­ку и пре­ры­вание по­то­ка вы­полнения ле­жит ис­клю­чи­тель­но на нас. Ес­ли мы за­бу­дем это сде­лать, то в ито­ге по­лу­чим со­всем не то, на что рас­счи­ты­ва­ли. Дру­гой боль­шой недоста­ток это­го под­хо­да со­сто­ит в том, что на­пи­сание ко­да, «очи­щаю­ще­го» ре­сур­сы, ста­но­вит­ся непро­стой за­да­чей (воз­мож­ные ме­то­ды ре­шения этой за­да­чи см. во врез­ке «По­лез­ные за­мет­ки»). Есть у это­го под­хо­да и еще один недоста­ток, осо­бен­но за­мет­ный при его сравнении с под­хо­дом на осно­ве ис­клю­чений: ра­бо­та с воз­вра­щае­мым зна­чением да­ет нам слиш­ком ма­ло ин­фор­ма­ции о том, что именно не заладилось. Ра­бо­тая с ис­клю­чения­ми, мы мо­жем по­лу­чить та­кие ат­ри­бу­ты, как тип ошиб­ки (он же код ошиб­ки); текст, опи­сы­ваю­щий про­бле­му; ме­сто, где про­изош­ла ошиб­ка. Всей этой до­полнитель­ной ин­фор­ма­ции при ра­бо­те с ко­да­ми оши­бок у нас нет.

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

Для понимания то­го, как про­ис­хо­дит по­иск, раз­бе­рем­ся, как осу­ще­ст­в­ля­ет­ся вы­полнение ка­ко­го-ли­бо при­ло­жения. Единицей вы­полнения всегда яв­ля­ет­ся функ­ция. Вы­полнение при­ло­жения на­чи­на­ет­ся с неко­то­рой функ­ции, на­зы­вае­мой точ­кой вхо­да [entry point]; в про­цес­се ра­бо­ты эта функ­ция мо­жет вы­зы­вать дру­гие функ­ции, а те, в свою оче­редь, мо­гут вы­зы­вать еще ка­кие-ли­бо функ­ции, и т. д. В лю­бой мо­мент вре­мени со­стояние вы­полнения про­грам­мы со­дер­жит, по­ми­мо ука­за­те­ля команд, и ин­фор­ма­цию о том, ка­кие функ­ции бы­ли вы­зва­ны пе­ред тем, как вы­полнение про­грам­мы при­шло в те­ку­щую точ­ку (на ко­то­рую ука­зы­ва­ет ука­за­тель команд). Ес­ли во вре­мя вы­полнения про­грам­мы про­ис­хо­дит ис­клю­чение, нор­маль­ное вы­полнение про­грам­мы приоста­нав­ли­ва­ет­ся (сре­дой вы­полнения) и на­чи­на­ет­ся рас­крут­ка [unwind] сте­ка в по­ис­ках пер­вого под­хо­дя­ще­го об­ра­бот­чика возник­ше­го ис­клю­чения. По­иск идет сле­дую­щим об­ра­зом: сна­ча­ла мы про­смат­ри­ваем спи­сок за­ре­ги­ст­ри­ро­ван­ных об­ра­бот­чи­ков в те­ку­щей точ­ке вы­полнения. Ес­ли сре­да вы­полнения на­шла под­хо­дя­щий об­ра­бот­чик, то вы­полнение про­грам­мы пе­ре­да­ет­ся на него; по­сле об­ра­бот­ки ис­клю­чения (ес­ли во вре­мя об­ра­бот­ки не бы­ло сгенери­ро­ва­но ника­ко­го ис­клю­чения) вы­полнение про­грам­мы про­дол­жа­ет­ся в нор­маль­ном ре­жи­ме. Ес­ли сре­да вы­полнения под­хо­дя­щий об­ра­бот­чик не на­шла или ес­ли во вре­мя ра­бо­ты об­ра­бот­чи­ка бы­ло сгенери­ро­ва­но это же или ка­кое-ли­бо дру­гое ис­клю­чение, сре­да вы­полнения пе­ре­хо­дит в точ­ку про­грам­мы, из ко­то­рой бы­ла вы­зва­на дан­ная функ­ция, и про­дол­жа­ет по­иск там. Эта точ­ка про­грам­мы при­над­ле­жит неко­то­рой ро­ди­тель­ской функ­ции, т. е. функ­ции, в хо­де вы­полнения ко­то­рой бы­ла вы­зва­на дан­ная функ­ция.

По­лез­ные за­мет­ки

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

int descr1, descr2;

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

if (-1 == descr1) return;

descr2 = open(“file2.dat”, 0_RDWR);

if (-1 == descr2) return;

some_task(descr1, descr2);

close(descr2);

close(descr1);

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

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);

}

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

int descr1, descr2;

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

if (-1 == descr1) return;

descr2 = open(“file2.dat”, 0_RDWR);

if (-1 == descr2) {

close(descr1);

return;

}

some_task(descr1, descr2);

close(descr2);

close(descr1);

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

int descr1, descr2;

descr1 = -1;

descr2 = -1;

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

if (-1 == descr1) goto CLEANUP;

descr2 = open(“file2.dat”, 0_RDWR);

if (-1 == descr1) goto CLEANUP;

some_task(descr1, descr2);

CLEANUP:

if (-1 != descr2) close(descr2);

if (-1 != descr1) close(descr1);

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

Да­вай­те по­смот­рим, что же для об­ра­бот­ки оши­бок и «очи­ст­ки» ре­сур­сов у нас есть в язы­ке Erlang. А в язы­ке Erlang оди­на­ко­во ши­ро­ко ис­поль­зу­ют­ся оба под­хо­да: и ко­ды оши­бок и ис­клю­чения вре­мени вы­полнения. Под­ход с ис­поль­зо­ванием ко­да ошиб­ки в язы­ке Erlang несколь­ко от­ли­ча­ет­ся от «клас­си­че­­ско­­го» под­хо­да: вме­сто ис­поль­зо­вания от­дель­ной гло­баль­ной пе­ре­мен­ной для хранений ко­да ошиб­ки, мно­гие BIF и функ­ции в слу­чае успе­ха и неуда­чи со­хра­ня­ют раз­ные объ­ек­ты. Раз­ли­чить си­туа­ции успеш­но­го и неуспеш­но­го вы­полнения лег­ко при по­мо­щи опе­ра­ции со­от­вет­ст­вия шаб­ло­ну [pattern-matching]. Так, на­при­мер, функ­ция file:open/2 мо­ду­ля file ис­поль­зу­ет­ся для то­го, что­бы от­крыть ка­кой-ли­бо файл пе­ред тем, как на­чать ра­бо­ту с этим фай­лом. Ес­ли эта опе­ра­ция вы­полнит­ся успеш­но, то бу­дет воз­вра­щен кор­теж ви­да {ok, IoDevice}, где IoDevice – неко­то­рый опи­са­тель фай­ла, при­ме­няе­мый для дальней­ше­го досту­па к нему. Ес­ли эта опе­ра­ция вы­полнит­ся с ошиб­кой, бу­дет воз­вра­щен кор­теж ви­да {error, Reason}, где Reason – неко­то­рый объ­ект, опи­сы­ваю­щий, что по­шло не так. С дру­гой сто­ро­ны, мно­гие BIF и функ­ции генери­ру­ют ис­клю­чение, ес­ли что-то по­шло не так во вре­мя их вы­полнения. Так, на­при­мер, ес­ли мы возь­мем BIF integer_to_list/1 и пе­ре­да­дим ей в ка­че­­ст­ве ар­гу­мен­та атом ab, то мы по­лу­чим в ре­зуль­та­те ошиб­ку вре­мени вы­полнения badarg, оз­на­чаю­щую, что дан­ный ар­гу­мент для функ­ции не кор­рек­тен.

Ис­поль­зо­вание ко­дов оши­бок доста­точ­но оче­вид­но и не тре­бу­ет спе­ци­аль­но­го син­так­си­са, в от­ли­чие от ра­бо­ты с ис­клю­чения­ми. В язы­ке Erlang ис­клю­чения бы­ва­ют трех раз­ных клас­сов: error, exit, throw. Класс ис­клю­чения оп­ре­де­ля­ет­ся тем, при по­мо­щи ка­ко­го BIF бы­ло сгенери­ро­ва­но ис­клю­чение. Ес­ли ис­клю­чение сгенери­ро­ва­но при по­мо­щи од­ной из BIF error/1 или error/2, то класс ис­клю­чения бу­дет error; ес­ли ис­клю­чение сгенери­ро­ва­но при по­мо­щи BIF exit/1, то класс ис­клю­чения бу­дет exit; ес­ли ис­клю­чение сгенери­ро­ва­но при по­мо­щи BIF throw/1, то класс ис­клю­чения бу­дет throw. Ис­клю­чения, генери­руе­мые сре­дой вы­полнения Erlang, BIF и биб­лио­теч­ны­ми функ­ция­ми, всегда име­ют класс error; мы же в сво­ем ко­де мо­жем генери­ро­вать ис­клю­чения лю­бо­го из трех клас­сов. Генери­ро­вать ис­клю­чения са­мо­стоя­тель­но мы уже нау­чи­лись; сле­дую­щий шаг – по­нять, как мы мо­жем ра­бо­тать со сгенери­ро­ван­ны­ми ис­клю­чения­ми.

Наи­бо­лее про­стая кон­ст­рук­ция для этой це­ли – catch Expr, где Expr – про­из­воль­ное вы­ра­жение. Ес­ли во вре­мя вы­чис­ления вы­ра­жения Expr ника­ко­го ис­клю­чения не бу­дет сгенери­ро­ва­но, то зна­чением вы­ра­жения catch Expr бу­дет зна­чение вы­ра­жения Expr. Ес­ли же во вре­мя вы­чис­ления вы­ра­жения бу­дет сгенери­ро­ва­но ис­клю­чение, то зна­чение вы­ра­жения бу­дет за­ви­сеть от клас­са сгенери­ро­ван­но­го ис­клю­чения. Ес­ли ис­клю­чение име­ет класс error, то вы­ра­жение catch Expr вернет {'EXIT’, {Reason, Stack}}, где Reason – при­чи­на возник­но­вения ис­клю­чения (или ар­гу­мент вы­зо­ва error/1), Stack – воз­врат по сте­ку [stacktrace] до мес­та возник­но­вения ис­клю­чения. Ес­ли ис­клю­чение име­ет класс exit (это оз­на­ча­ет, что где-то в ко­де был сде­лан вы­зов exit(Term)), то вы­ра­жение catch Expr вернет {'EXIT’, Term}. Ес­ли ис­клю­чение име­ет класс throw (это оз­на­ча­ет, что где-то в ко­де был сде­лан вы­зов throw(Term)), то вы­ра­жение catch Expr вернет Term. Оче­вид­но, что ис­поль­зо­вание вы­ра­жения catch Expr по­зво­ля­ет по­крыть все воз­мож­ные про­бле­мы ра­бо­ты с ис­клю­чения­ми, но ис­поль­зо­вать это вы­ра­жение не всегда удоб­но. По­это­му в язы­ке Erlang есть улуч­шен­ный ва­ри­ант вы­ра­жения catch Expr: это вы­ра­жение try/catch. Ба­зо­вая вер­сия это­го вы­ра­жения име­ет сле­дую­щий вид:

try Exprs

catch

[Class1:]ExceptionPattern1 [when ExceptionGuard1] → CatchBody1;

...

[ClassN:]ExceptionPatternN [when ExceptionGuardN] → CatchBodyN

end

Здесь Exprs – по­сле­до­ва­тель­ность вы­ра­жений, в ко­то­рых мо­жет быть сгенери­ро­ва­но ис­­клю­чение, Classi – один из трех воз­мож­ных клас­сов ис­клю­чений, ExceptionPatterni – вы­ра­жение со­от­вет­ст­вия шаб­ло­ну для от­лав­ли­вае­мо­го ис­клю­чения, ExceptionGuradi –вы­ра­жение ох­ра­ны для от­лав­ли­вае­мо­го ис­клю­чения, CatchBodyi – те­ло об­ра­бот­чи­ка ис­клю­чения. Класс ис­клю­чения и вы­ра­жения ох­ра­ны яв­ля­ют­ся необя­за­тель­ны­ми эле­мен­та­ми при за­дании об­ра­бот­чи­ка. Зна­чение вы­ра­жения try/catch вы­чис­ля­ет­ся сле­дую­щим об­ра­зом. Ес­ли во вре­мя вы­чис­ления по­сле­до­ва­тель­но­сти вы­ра­жений Exprs ника­ко­го ис­клю­чения не бы­ло сгенери­ро­ва­но, то зна­чением вы­ра­жения try/catch бу­дет зна­чение по­сле­до­ва­тель­но­сти вы­ра­жений Exprs. Ес­ли во вре­мя вы­чис­ления по­сле­до­ва­тель­но­сти вы­ра­жений сгенери­ру­ет­ся ис­клю­чение, то сре­да вы­полнения Erlang бу­дет ис­кать пер­вый под­хо­дя­щий об­ра­бот­чик по­сле­до­ва­тель­но сре­ди об­ра­бот­чи­ков бло­ка catch. По на­хо­ж­дении пер­во­го под­хо­дя­ще­го об­ра­бот­чи­ка по­иск сре­ди об­ра­бот­чи­ков бло­ка catch пре­кра­ща­ет­ся, и зна­чение вы­ра­жения те­ла об­ра­бот­чи­ка CatchBodyi бу­дет зна­чением все­го вы­ра­жения try/catch. Ес­ли же под­хо­дя­ще­го об­ра­бот­чи­ка сре­ди об­ра­бот­чи­ков бло­ка catch не об­на­ру­жит­ся, то сгенери­ро­ван­ное ис­клю­чение вый­дет за пре­де­лы вы­ра­жения try/catch. Су­ще­ст­ву­ет бо­лее слож­ный ва­ри­ант вы­ра­жения try/catch, ко­то­рый яв­ля­ет­ся гиб­ри­дом вы­ра­жения try/catch в про­стом ва­ри­ан­те и вы­ра­жения case для зна­чения по­сле­до­ва­тель­но­сти вы­ра­жений Exprs. Об этом ва­ри­ан­те вы­ра­жения try/catch мы по­го­во­рим бо­лее под­роб­но во вре­мя од­но­го из прак­ти­ку­мов (ин­те­ре­сую­щие­ся чи­та­те­ли мо­гут по­смот­реть до­ку­мен­та­цию к язы­ку Erlang).

У нас остал­ся еще один не за­тро­ну­тый по­ка во­прос, свя­зан­ный с ра­бо­той с ис­клю­чения­ми: «очи­ст­ка» ре­сур­сов. Для это­го вы­ра­жение try/catch со­дер­жит спе­ци­аль­ный блок (в вы­ра­жении catch Expr та­ко­го бло­ка нет, по ло­ги­ке ра­бо­ты это­го вы­ра­жения он там и не ну­жен): это блок after. Ло­ги­ка вы­чис­ления вы­ра­жения try/catch не ме­ня­ет­ся в за­ви­си­мо­сти от то­го, есть блок after в этом вы­ра­жении или нет. Это оз­на­ча­ет, что блок after ну­жен толь­ко для очи­ст­ки ре­сур­сов и зна­чение по­сле­до­ва­тель­но­сти вы­ра­жений в бло­ке after «те­ря­ет­ся» по­сле вы­полнения это­го бло­ка. Да­вай­те на неболь­шом при­ме­ре рас­смот­рим, как ис­поль­зо­вать блок after:

{ok, File} = file:open(FileName, [read, binary])

try

{ok, Data} = file:read(File, Size),

binary_to_term(Data)

after

file:close(File)

end

В этом при­ме­ре мы от­кры­ва­ем файл (до на­ча­ла вы­ра­жения try/catch), чи­та­ем из фай­ла дво­ич­ные дан­ные неко­то­ро­го пре­до­пре­де­лен­но­го раз­ме­ра, де­се­риа­ли­зу­ем из этих дан­ных неко­то­рый объ­ект Erlang и за­кры­ва­ем файл. Ес­ли во вре­мя чтения дан­ных из фай­ла или де­се­риа­ли­за­ции бу­дет сгенери­ро­ва­но ис­клю­чение, то файл бу­дет за­крыт бла­го­да­ря то­му, что код за­кры­тия на­хо­дит­ся в бло­ке after. Ес­ли все опе­ра­ции вы­пол­нят­ся успеш­но, то файл так­же бу­дет за­крыт.

До сих пор, го­во­ря о ра­бо­те с ис­клю­чения­ми, мы под­ра­зу­ме­ва­ли, что все дей­ст­вия про­ис­хо­дят в од­ном про­цес­се Erlang. Те­перь да­вай­те по­го­во­рим о ра­бо­те с ис­клю­чения­ми в си­туа­ции, когда у нас вы­пол­ня­ет­ся несколь­ко про­цес­сов Erlang – как на од­ном уз­ле (или в пре­де­лах од­но­го эк­зем­п­ля­ра сре­ды вы­полнения Erlang), так и на несколь­ких. Оче­вид­но, что ес­ли в ка­ком-ли­бо про­цес­се Erlang во вре­мя вы­полнения ко­да бу­дет сгенери­ро­ва­но ис­клю­чение, и в том же про­цес­се это ис­клю­чение бу­дет об­ра­бо­та­но, то для дру­гих про­цес­сов Erlang дан­ное про­ис­ше­ст­вие ока­жет­ся неза­ме­чен­ным. По­это­му в дальней­шем мы бу­дем рас­смат­ри­вать толь­ко си­туа­ции, когда ис­клю­чения в ка­ком-ли­бо про­цес­се генери­ру­ют­ся и не об­ра­ба­ты­ва­ют­ся. Возника­ет вполне ло­гич­ный во­прос: а что в та­ком слу­чае про­изой­дет с са­мим про­цес­сом и дру­ги­ми про­цес­са­ми? Про­цесс, в ко­то­ром бу­дет сгенери­ро­ва­но необ­ра­ба­ты­вае­мое ис­клю­чение, бу­дет за­вер­шен сре­дой вы­полнения Erlang, что оче­вид­но. Все осталь­ные про­цес­сы, ес­ли они не бы­ли свя­за­ны с за­вер­шен­ным про­цес­сом, про­дол­жа­ют свою ра­бо­ту неза­ви­си­мо­ от то­го, в од­ном ли эк­зем­п­ля­ре сре­ды вы­полнения Erlang они ра­бо­та­ли или в раз­ных. Бо­лее то­го, они да­же никак не уз­на­ют об этом со­бы­тии, ес­ли толь­ко са­ми не за­про­сят ин­фор­ма­цию о про­цес­се (ис­поль­зуя для это­го од­ну из BIF process_info/1 или process_info/2). Та­ким об­ра­зом, мож­но ска­зать, что про­цес­сы в язы­ке Erlang неза­ви­си­мы друг от дру­га с точ­ки зрения необ­ра­бо­тан­ных ис­клю­чений.

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

Наи­бо­лее ин­те­ре­сен для нас во­прос, что про­ис­хо­дит со свя­зан­ны­ми про­цес­са­ми, ес­ли один из них за­кан­чи­ва­ет свою жизнь (как в слу­чае обыч­но­го за­вер­шения ра­бо­ты, так и в слу­чае необ­ра­бо­тан­но­го ис­клю­чения). Для от­ве­та на этот во­прос сле­ду­ет сде­лать сле­дую­щее за­ме­чание: все про­цес­сы мож­но раз­де­лить на две груп­пы: обыч­ные про­цес­сы и про­цес­сы-су­пер­ви­зо­ры. Когда ка­кой-ли­бо про­цесс за­вер­ша­ет свою ра­бо­ту, то про­цесс-су­пер­ви­зор (свя­зан­ный с этим про­цес­сом) всегда по­лу­ча­ет со­об­щение {‘EXIT’, FromPid, Reason}. Здесь FromPid – это иден­ти­фи­ка­тор про­цес­са, за­вер­шив­ше­го свою ра­бо­ту; Reason – при­чи­на, по ко­то­рой про­цесс за­вер­шил ра­бо­ту. Ес­ли про­цесс за­вер­шил свою ра­бо­ту ес­те­ст­вен­ным спо­со­бом (когда за­вер­ша­ет­ся вы­полнение функ­ции про­цес­са), то при­чи­на Reason за­вер­шения про­цес­са бу­дет normal. Точ­но та­кая же при­чи­на бу­дет, ес­ли про­цесс сгенери­ру­ет необ­ра­бо­тан­ное ис­клю­чение вы­зо­вом exit(normal); или ес­ли ка­кой-ли­бо дру­гой про­цесс сде­ла­ет вы­зов exit(Pid, normal), где Pid – иден­ти­фи­ка­тор за­вер­шае­мо­го про­цес­са (все это счи­та­ет­ся ес­те­ст­вен­ным за­вер­шением ра­бо­ты про­цес­са). Ес­ли про­цесс сам за­вер­шил свою ра­бо­ту, сгенери­ро­вав необ­ра­бо­тан­ное ис­клю­чение вы­зо­вом exit(Reason); или ес­ли ка­кой-ли­бо дру­гой про­цесс сде­ла­ет вы­зов exit(Pid, Reason), то при­чи­ной за­вер­шения про­цес­са бу­дет со­от­вет­ст­вую­щий ар­гу­мент BIF exit/1 или exit/2. Ес­ли про­цесс сгенери­ру­ет необ­ра­бо­тан­ное ис­клю­чение клас­са error или клас­са throw, то при­чи­на за­вер­шения про­цес­са бу­дет иметь сле­дую­щий бо­лее слож­ный вид: {Reason, Stack}. Здесь Reason – при­чи­на за­вер­шения, а Stack – стек­трейс [stacktrace], ука­зы­ваю­щий на ме­сто генера­ции это­го ис­клю­чения.

Те­перь да­вай­те по­го­во­рим об обыч­ных про­цес­сах. Ес­ли про­цесс, свя­зан­ный с обыч­ным про­цес­сом, за­вер­ша­ет­ся ес­те­ст­вен­ным об­ра­зом, то с та­ким обыч­ным про­цес­сом ниче­го не про­ис­хо­дит: он про­дол­жа­ет свое вы­полнение и ника­ких со­об­щений не по­лу­ча­ет. Ес­ли же про­цесс, свя­зан­ный с обыч­ным про­цес­сом, за­вер­ша­ет­ся из-за необ­ра­бо­тан­но­го ис­клю­чения или ес­ли этот про­цесс за­вер­ша­ет ка­кой-ли­бо дру­гой про­цесс вы­зо­вом BIF exit/2 с при­чи­ной за­вер­шения, от­лич­ной от normal, то свя­зан­ный обыч­ный про­цесс так­же бу­дет за­вер­шен. Та­кое за­вер­шение свя­зан­ного обыч­но­го про­цес­са не яв­ля­ет­ся ес­те­ст­вен­ным (это оз­на­ча­ет, что при­чи­на за­вер­шения свя­зан­но­го обыч­но­го про­цес­са от­лич­на от normal).

Рас­смот­рим неболь­шой при­мер, ил­лю­ст­ри­рую­щий опи­сан­ное вы­ше по­ве­дение. Пусть у нас про­цесс A свя­зан с про­цес­са­ми B и C, а про­цесс D свя­зан с про­цес­сом B. Пусть все про­цес­сы яв­ля­ют­ся обыч­ны­ми. Рас­смот­рим си­туа­цию, когда про­цесс B за­вер­ша­ет­ся ес­те­ст­вен­ным спо­со­бом. В этом слу­чае про­цес­сы A, C и D оста­ют­ся «жи­вы­ми», при­чем про­цес­сы A и C оста­ют­ся свя­зан­ны­ми, а про­цесс D ста­но­вит­ся обо­соб­лен­ным. Те­перь рас­смот­рим си­туа­цию, когда про­цесс B за­вер­ша­ет­ся спо­со­бом, от­лич­ным от ес­те­ст­вен­но­го за­вер­шения. В этом слу­чае все про­цес­сы, свя­зан­ные с B (это A и D), за­вер­ша­ют­ся, при­во­дя к то­му, что за­вер­ша­ют­ся и все про­цес­сы, свя­зан­ные с A или D – это про­цесс C. Вид­но, что ес­ли у нас есть граф свя­зан­ных обыч­ных про­цес­сов, то в слу­чае неес­те­ст­вен­но­го за­вер­шения од­но­го из про­цес­сов за­вер­шат­ся все про­цес­сы в этом гра­фе.

Пусть те­перь у нас про­цесс A яв­ля­ет­ся су­пер­ви­зо­ром. Рас­смот­рим си­туа­цию, когда про­цесс B за­вер­ша­ет­ся ес­те­ст­вен­ным спо­со­бом. В этом слу­чае про­цес­сы A, C и D оста­ют­ся «жи­вы­ми», при этом про­цес­сы A и C оста­ют­ся свя­зан­ны­ми, а про­цесс D ста­но­вит­ся обо­соб­лен­ным про­цес­сом. Про­цесс A по­лу­чит со­об­щение {‘EXIT’, PidB, normal}, где PidB – иден­ти­фи­ка­тор про­цес­са B. Те­перь рас­смот­рим си­туа­цию, когда про­цесс B за­вер­ша­ет­ся спо­со­бом, от­лич­ным от ес­те­ст­вен­но­го за­вер­шения. В этом слу­чае все обыч­ные про­цес­сы, свя­зан­ные с B, за­вер­ша­ют­ся – это про­цесс D; при этом про­цесс A по­лу­чит со­об­щение {‘EXIT’, PidB, ReasonB} (здесь ReasonB – при­чи­на за­вер­шения про­цес­са B), а про­цесс C «останет­ся в жи­вых» (и не по­лу­чит ника­ких со­об­щений). Вид­но, что про­цесс-су­пер­ви­зор, по­ми­мо воз­мож­но­сти по­лу­чения уве­дом­лений о за­вер­шении свя­зан­ных с ним про­цес­сов и воз­мож­ной ре­ак­ции на эти уве­дом­ления, эк­раниру­ет про­цесс из од­ной вет­ви гра­фа свя­зан­ных про­цес­сов от из­менений в жизни про­цес­сов из дру­гих вет­вей гра­фа свя­зан­ных про­цес­сов. Разница в по­ве­дении ме­ж­ду обыч­ны­ми про­цес­са­ми и про­цес­са­ми-су­пер­ви­зо­ра­ми за­клю­ча­ет­ся еще и в ре­ак­ции на по­пыт­ку ка­ким-ли­бо про­цес­сом за­вер­шить дан­ный про­цесс при по­мо­щи BIF exit/2. Ес­ли мы пы­та­ем­ся за­вер­шить обыч­ный про­цесс при по­мо­щи вы­зо­ва exit(Pid, Reason), то этот про­цесс за­вер­ша­ет­ся с при­чи­ной за­вер­шения Reason. Ес­ли же мы пы­та­ем­ся за­вер­шить про­цесс-су­пер­ви­зор при по­мо­щи вы­зо­ва exit(Pid, Reason), то этот про­цесс по­лу­чит со­об­щение {‘EXIT’, FromPid, Reason} и про­дол­жит свое вы­полнение. Здесь FromPid – иден­ти­фи­ка­тор про­цес­са, пы­тав­ше­го­ся за­вер­шить дан­ный про­цесс при по­мо­щи вы­зо­ва exit(Pid, Reason). Ес­ли мы хо­тим за­вер­шить про­цесс-су­пер­ви­зор из дру­го­го про­цес­са, то де­лать это сле­ду­ет при по­мо­щи сле­дую­ще­го вы­зо­ва: exit(Pid, kill). Тогда при­чи­на за­вер­шения та­ко­го про­цес­са (неза­ви­си­мо от то­го, яв­ля­ет­ся ли он су­пер­ви­зо­ром или нет) бу­дет сле­дую­щая: killed.

Те­перь да­вай­те по­го­во­рим о функ­ци­ях (точнее, о BIF), ко­то­рые мы бу­дем при­ме­нять для соз­дания свя­зей и управ­ления, яв­ля­ет­ся ли про­цесс су­пер­ви­зо­ром или обыч­ным про­цес­сом. Вы­зов process_flag(trap_exit, true) по­зво­ля­ет про­цес­су ука­зать, что этот про­цесс дол­жен быть су­пер­ви­зо­ром; вы­зов process_flag(trap_exit, false) ука­зы­ва­ет, что он дол­жен быть обыч­ным про­цес­сом. Су­ще­ст­ву­ет ва­ри­ант BIF process_flag/3, по­зво­ляю­щий оп­ре­де­лить, яв­ля­ет­ся ли про­цесс су­пер­ви­зо­ром или обыч­ным про­цес­сом для лю­бо­го дру­го­го про­цес­са. Соз­дать связь ме­ж­ду те­ку­щим про­цес­сом и про­цес­сом, за­дан­ным по его иден­ти­фи­ка­то­ру, мож­но при по­мо­щи BIF link/1; раз­ры­ва­ет­ся дан­ная связь при по­мо­щи BIF unlink/1. Соз­дать но­вый про­цесс и сра­зу же свя­зать его с те­ку­щим про­цес­сом мож­но при по­мо­щи се­мей­ст­ва BIF spawn_link/1,2,3,4. Глав­ное от­ли­чие функ­ций се­мей­ст­ва spawn_link/1,2,3,4 от по­сле­до­ва­тель­но­го ис­поль­зо­вания функ­ций се­мей­ст­ва spawn/1,2,3,4 и функ­ции link/1, т. е. от по­сле­до­ва­тель­но­го соз­дания но­во­го про­цес­са и свя­зи ме­ж­ду те­ку­щим и но­вым про­цес­са­ми, в том, что функ­ции се­мей­ст­ва spawn_link/1,2,3,4 соз­да­ют но­вый про­цесс и связь ме­ж­ду но­вым и те­ку­щим про­цес­са­ми ато­мар­но.

В язы­ке Erlang су­ще­ст­ву­ет аль­тер­на­ти­ва свя­зям – это монито­ры. Монитор – это од­но­на­прав­лен­ная связь ме­ж­ду про­цес­са­ми, слу­жа­щая толь­ко для пе­ре­да­чи со­об­щения о пре­кра­щении ра­бо­ты про­цес­са. Это оз­на­ча­ет, что вне за­ви­си­мо­сти от то­го, яв­ля­ет­ся ли про­цесс, соз­дав­ший монитор к дру­го­му про­цес­су, су­пер­ви­зо­ром или обыч­ным про­цес­сом, он всегда бу­дет толь­ко по­лу­чать со­об­щение о пре­кра­щении ра­бо­ты дру­го­го про­цес­са. Это со­об­щение име­ет сле­дую­щий вид: {‘DOWN’, Ref, process, Pid2, Reason}, где Ref – это объ­ект (ти­па ссыл­ка), по­лу­чае­мый при соз­дании монито­ра, Pid2 – иден­ти­фи­ка­тор про­цес­са, к ко­то­ро­му соз­дан монитор, Reason – при­чи­на за­вер­шения про­цес­са. Ес­ли про­цесс соз­да­ет монитор к несу­ще­ст­вую­ще­му монито­ру, этот про­цесс немед­лен­но по­лу­чит при­ве­ден­ное вы­ше со­об­щение с при­чи­ной noproc. Для соз­дания монито­ра ис­поль­зу­ет­ся BIF monitor/2; для унич­то­жения монито­ра ис­поль­зу­ет­ся од­на из BIF demonitor/1 или demonitor/2. Для ато­мар­но­го соз­дания но­во­го про­цес­са и монито­ра к нему ис­поль­зу­ет­ся се­мей­ст­во BIF spawn_monitor/1,2.

В дан­ной ста­тье мы рас­смот­ре­ли та­кую важ­ную часть для по­строения рас­пре­де­лен­ных сис­тем, как от­ка­зоустой­чи­вость. А в сле­дую­щем но­ме­ре мы непо­сред­ст­вен­но пе­рей­дем к рас­смот­рению те­мы о соз­дании рас­пре­де­лен­ных при­ло­жений на язы­ке Erlang. |

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