Erlang入門(四)——錯誤處理和魯棒性
去了趟福州,事情沒搞定,托給同學幫忙處理了,回家休息了兩天就來上班了。回家這幾天最大的收獲是第四次重讀《深入Java虛擬機》,以前不大明了的章節豁然開朗,有種開竅的感覺,水到渠成,看來技術的學習還是急不來。閑話不提,繼續Erlang的學習,上次學習到分布式編程的章節,剩下三章分別是錯誤處理、構造健壯的係統和雜項,錯誤處理和構造健壯的係統今天一起讀了,僅摘記下。
任何一門語言都有自己的錯誤處理機製,Erlang也不例外,語法錯誤編譯器可以幫你指出,而邏輯錯誤和運行時錯誤就隻有靠程序員利用Erlang提供的機製來妥善處理,放置程序的崩潰。
Erlang的機製有:
1)監控某個表達式的執行
2)監控其他進程的行為
3)捕捉未定義函數執行錯誤等
一、catch和throw語句
調用某個會產生錯誤的表達式會導致調用進程的非正常退出,比如錯誤的模式匹配(2=3),這種情況下可以用catch語句:
catch expression
試看一個例子,一個函數foo:
foo(1) ->
hello;
foo(2) ->
throw({myerror, abc});
foo(3) ->
tuple_to_list(a);
foo(4) ->
exit({myExit, 222}).
hello;
foo(2) ->
throw({myerror, abc});
foo(3) ->
tuple_to_list(a);
foo(4) ->
exit({myExit, 222}).
當沒有使用catch的時候,假設有一個標識符為Pid的進程調用函數foo(在一個模塊中),那麼:
foo(1) - 返回hello
foo(2) - 語句throw({myerror, abc})執行,因為我們沒有在一個catch中調用foo(2),因此進程Pid將因為錯誤而終止。
foo(3) - tuple_to_list將一個元組轉化為列表,因為a不是元組,因此進程Pid同樣因為錯誤而終止
foo(4) - 因為沒有使用catch,因此foo(4)調用了exit函數將使進程Pid終止,{myExit, 222} 參數用於說明退出的原因。
foo(5) - 進程Pid將因為foo(5)的調用而終止,因為沒有和foo(5)匹配的函數foo/1。
讓我們看看用catch之後是什麼樣:
demo(X) ->
case catch foo(X) of
{myerror, Args} ->
{user_error, Args};
{'EXIT', What} ->
{caught_error, What};
Other ->
Other
end.
再看看結果,case catch foo(X) of
{myerror, Args} ->
{user_error, Args};
{'EXIT', What} ->
{caught_error, What};
Other ->
Other
end.
demo(1) - 沒有錯誤發生,因此catch語句將返回表達式結果hello
demo(2) - foo(2)拋出錯誤{myerror, abc},被catch返回,因此將返回{user_error,abc}
demo(3) - foo(3)執行失敗,因為參數錯誤,因此catch返回{'EXIT',badarg'},最後返回{caught_error,badarg}
demo(4) - 返回{caught_error,{myexit,222}}
demo(5) - 返回{caught_error,function_clause}
使用catch和throw可以將可能產生錯誤的代碼包裝起來,throw可以用於尾遞歸的退出等等。Erlang是和scheme一樣進行尾遞歸優化的,它們都沒有顯式的迭代結構(比如for循環)
二、進程的終止
在進程中調用exit的BIFs就可以顯式地終止進程,exit(normal)表示正常終止,exit(Reason)通過Reason給出非正常終止的原因。進程的終止也完全有可能是因為運行時錯誤引起的。
三、連接的進程
進程之間的連接是雙向的,也就是說進程A打開一個連接到B,也意味著有一個從B到A的連接。當進程終止的時候,有一個EXIT信號將發給所有與它連接的進程。信號的格式如下:
{'EXIT', Exiting_Process_Id, Reason}
Exiting_Process_Id 是指終止的進程標記符
Reason 是進程終止的原因。如果Reason是normal,接受這個信號的進程的默認行為是忽略這個信號。默認對Exit信號的處理可以被重寫,以允許進程對Exit信號的接受做出不同的反應。
1.連接進程:
通過link(Pid),就可以在調用進程與進程Pid之間建立連接
2.取消連接
反之通過unlink(Pid)取消連接。
3.創立進程並連接:
通過spawn_link(Module, Function, ArgumentList)創建進程並連接,該方法返回新創建的進程Pid
通過進程的相互連接,許多的進程可以組織成一個網狀結構,EXIT信號(非normal)從某個進程發出(該進程終止),所有與它相連的進程以及與這些進程相連的其他進程,都將收到這個信號並終止,除非它們實現了自定義的EXIT信號處理方法。一個進程鏈狀結構的例子:
-module(normal).
-export([start/1, p1/1, test/1]).
start(N) ->
register(start, spawn_link(normal, p1, [N - 1])).
p1(0) ->
top1();
p1(N) ->
top(spawn_link(normal, p1, [N - 1]),N).
top(Next, N) ->
receive
X ->
Next ! X,
io:format("Process ~w received ~w~n", [N,X]),
top(Next,N)
end.
top1() ->
receive
stop ->
io:format("Last process now exiting ~n", []),
exit(finished);
X ->
io:format("Last process received ~w~n", [X]),
top1()
end.
test(Mess) ->
start ! Mess.
-export([start/1, p1/1, test/1]).
start(N) ->
register(start, spawn_link(normal, p1, [N - 1])).
p1(0) ->
top1();
p1(N) ->
top(spawn_link(normal, p1, [N - 1]),N).
top(Next, N) ->
receive
X ->
Next ! X,
io:format("Process ~w received ~w~n", [N,X]),
top(Next,N)
end.
top1() ->
receive
stop ->
io:format("Last process now exiting ~n", []),
exit(finished);
X ->
io:format("Last process received ~w~n", [X]),
top1()
end.
test(Mess) ->
start ! Mess.
執行:
> normal:start(3).
true
> normal:test(123).
Process 2 received 123
Process 1 received 123
Last process received 123
> normal:test(stop).
Process 2 received stop
Process 1 received stop
Last process now exiting
stop
true
> normal:test(123).
Process 2 received 123
Process 1 received 123
Last process received 123
> normal:test(stop).
Process 2 received stop
Process 1 received stop
Last process now exiting
stop
四、運行時失敗
一個運行時錯誤將導致進程的非正常終止,伴隨著非正常終止EXIT信號將發出給所有連接的進程,EXIT信號中有Reason並且Reason中包含一個atom類型用於說明錯誤的原因,常見的原因如下:
badmatch - 匹配失敗,比如一個進程進行1=3的匹配,這個進程將終止,並發出{'EXIT', From, badmatch}信號給連接的進程
badarg - 顧名思義,參數錯誤,比如atom_to_list(123),數字不是atom,因此將發出{'EXIT', From, badarg}信號給連接進程
case_clause - 缺少分支匹配,比如
M = 3,
case M of
1 ->
yes;
2 ->
no
end.
沒有分支3,因此將發出{'EXIT', From, case_clause}給連接進程case M of
1 ->
yes;
2 ->
no
end.
if_clause - 同理,if語句缺少匹配分支
function_clause - 缺少匹配的函數,比如:
foo(1) ->
yes;
foo(2) ->
no.
如果我們調用foo(3),因為沒有匹配的函數,將發出{'EXIT', From, function_clause} 給連接的進程。yes;
foo(2) ->
no.
undef - 進程執行一個不存在的函數
badarith - 非法的算術運算,比如1+foo。
timeout_value - 非法的超時時間設置,必須是整數或者infinity
nocatch - 使用了throw,沒有相應的catch去通訊。
五、修改默認的信號接收action
當進程接收到EXIT信號,你可以通過process_flag/2方法來修改默認的接收行為。執行process_flag(trap_exit,true)設置捕獲EXIT信號為真來改變默認行為,也就是將EXIT信號作為一般的進程間通信的信號進行接受並處理;process_flag(trap_exit,false)將重新開啟默認行為。
例子:
-module(link_demo).
-export([start/0, demo/0, demonstrate_normal/0, demonstrate_exit/1,
demonstrate_error/0, demonstrate_message/1]).
start() ->
register(demo, spawn(link_demo, demo, [])).
demo() ->
process_flag(trap_exit, true),
demo1().
demo1() ->
receive
{'EXIT', From, normal} ->
io:format("Demo process received normal exit from ~w~n",[From]),
demo1();
{'EXIT', From, Reason} ->
io:format("Demo process received exit signal ~w from ~w~n",[Reason, From]),
demo1();
finished_demo ->
io:format("Demo finished ~n", []);
Other ->
io:format("Demo process message ~w~n", [Other]),
demo1()
end.
demonstrate_normal() ->
link(whereis(demo)).
demonstrate_exit(What) ->
link(whereis(demo)),
exit(What).
demonstrate_message(What) ->
demo ! What.
demonstrate_error() ->
link(whereis(demo)),
1 = 2.
-export([start/0, demo/0, demonstrate_normal/0, demonstrate_exit/1,
demonstrate_error/0, demonstrate_message/1]).
start() ->
register(demo, spawn(link_demo, demo, [])).
demo() ->
process_flag(trap_exit, true),
demo1().
demo1() ->
receive
{'EXIT', From, normal} ->
io:format("Demo process received normal exit from ~w~n",[From]),
demo1();
{'EXIT', From, Reason} ->
io:format("Demo process received exit signal ~w from ~w~n",[Reason, From]),
demo1();
finished_demo ->
io:format("Demo finished ~n", []);
Other ->
io:format("Demo process message ~w~n", [Other]),
demo1()
end.
demonstrate_normal() ->
link(whereis(demo)).
demonstrate_exit(What) ->
link(whereis(demo)),
exit(What).
demonstrate_message(What) ->
demo ! What.
demonstrate_error() ->
link(whereis(demo)),
1 = 2.
創建的進程執行demo方法,demo方法中設置了trap_exit為true,因此,在receive中可以像對待一般的信息一樣處理EXIT信號,這個程序是很簡單了,測試看看:
> link_demo:start().
true
> link_demo:demonstrate_normal().
true
Demo process received normal exit from <0.13.1>
> link_demo:demonstrate_exit(hello).
Demo process received exit signal hello from <0.14.1>
** exited: hello **
> link_demo:demonstrate_exit(normal).
Demo process received normal exit from <0.13.1>
** exited: normal **
> link_demo:demonstrate_error().
!!! Error in process <0.17.1> in function
!!! link_demo:demonstrate_error()
!!! reason badmatch
** exited: badmatch **
Demo process received exit signal badmatch from <0.17.1>
true
> link_demo:demonstrate_normal().
true
Demo process received normal exit from <0.13.1>
> link_demo:demonstrate_exit(hello).
Demo process received exit signal hello from <0.14.1>
** exited: hello **
> link_demo:demonstrate_exit(normal).
Demo process received normal exit from <0.13.1>
** exited: normal **
> link_demo:demonstrate_error().
!!! Error in process <0.17.1> in function
!!! link_demo:demonstrate_error()
!!! reason badmatch
** exited: badmatch **
Demo process received exit signal badmatch from <0.17.1>
六、未定義函數和未注冊名字
1.當調用一個未定義的函數時,Mod:Func(Arg0,...,ArgN),這個調用將被轉為:
error_handler:undefined_function(Mod, Func, [Arg0,...,ArgN])
其中的error_handler模塊是係統自帶的錯誤處理模塊
2.當給一個未注冊的進程名發送消息時,調用將被轉為:
error_handler:unregistered_name(Name,Pid,Message)
3.如果不使用係統自帶的error_handler,可以通過process_flag(error_handler, MyMod) 設置自己的錯誤處理模塊。
七、Catch Vs. Trapping Exits
這兩者的區別在於應用場景不同,Trapping Exits應用於當接收到其他進程發送的EXIT信號時,而catch僅用於表達式的執行。
第8章介紹了如何利用錯誤處理機製去構造一個健壯的係統,用了幾個例子,我將8.2節的例子完整寫了下,並添加客戶端進程用於測試:
-module(allocator).
-export([start/1,server/2,allocate/0,free/1,start_client/0,loop/0]).
start(Resources) ->
Pid = spawn(allocator, server, [Resources,[]]),
register(resource_alloc, Pid).
%函數接口
allocate() ->
request(alloc).
free(Resource) ->
request({free,Resource}).
request(Request) ->
resource_alloc ! {self(),Request},
receive
{resource_alloc, error} ->
exit(bad_allocation); % exit added here
{resource_alloc, Reply} ->
Reply
end.
% The server.
server(Free, Allocated) ->
process_flag(trap_exit, true),
receive
{From,alloc} ->
allocate(Free, Allocated, From);
{From,{free,R}} ->
free(Free, Allocated, From, R);
{'EXIT', From, _ } ->
check(Free, Allocated, From)
end.
allocate([R|Free], Allocated, From) ->
link(From),
io:format("連接客戶端進程~w~n",[From]),
From ! {resource_alloc,{yes,R}},
server(Free, [{R,From}|Allocated]);
allocate([], Allocated, From) ->
From ! {resource_alloc,no},
server([], Allocated).
free(Free, Allocated, From, R) ->
case lists:member({R,From}, Allocated) of
true ->
From ! {resource_alloc,ok},
Allocated1 = lists:delete({R, From}, Allocated),
case lists:keysearch(From,2,Allocated1) of
false->
unlink(From),
io:format("從進程~w斷開~n",[From]);
_->
true
end,
server([R|Free],Allocated1);
false ->
From ! {resource_alloc,error},
server(Free, Allocated)
end.
check(Free, Allocated, From) ->
case lists:keysearch(From, 2, Allocated) of
false ->
server(Free, Allocated);
{value, {R, From}} ->
check([R|Free],
lists:delete({R, From}, Allocated), From)
end.
start_client()->
Pid2=spawn(allocator,loop,[]),
register(client, Pid2).
loop()->
receive
allocate->
allocate(),
loop();
{free,Resource}->
free(Resource),
loop();
stop->
true;
_->
loop()
end.
-export([start/1,server/2,allocate/0,free/1,start_client/0,loop/0]).
start(Resources) ->
Pid = spawn(allocator, server, [Resources,[]]),
register(resource_alloc, Pid).
%函數接口
allocate() ->
request(alloc).
free(Resource) ->
request({free,Resource}).
request(Request) ->
resource_alloc ! {self(),Request},
receive
{resource_alloc, error} ->
exit(bad_allocation); % exit added here
{resource_alloc, Reply} ->
Reply
end.
% The server.
server(Free, Allocated) ->
process_flag(trap_exit, true),
receive
{From,alloc} ->
allocate(Free, Allocated, From);
{From,{free,R}} ->
free(Free, Allocated, From, R);
{'EXIT', From, _ } ->
check(Free, Allocated, From)
end.
allocate([R|Free], Allocated, From) ->
link(From),
io:format("連接客戶端進程~w~n",[From]),
From ! {resource_alloc,{yes,R}},
server(Free, [{R,From}|Allocated]);
allocate([], Allocated, From) ->
From ! {resource_alloc,no},
server([], Allocated).
free(Free, Allocated, From, R) ->
case lists:member({R,From}, Allocated) of
true ->
From ! {resource_alloc,ok},
Allocated1 = lists:delete({R, From}, Allocated),
case lists:keysearch(From,2,Allocated1) of
false->
unlink(From),
io:format("從進程~w斷開~n",[From]);
_->
true
end,
server([R|Free],Allocated1);
false ->
From ! {resource_alloc,error},
server(Free, Allocated)
end.
check(Free, Allocated, From) ->
case lists:keysearch(From, 2, Allocated) of
false ->
server(Free, Allocated);
{value, {R, From}} ->
check([R|Free],
lists:delete({R, From}, Allocated), From)
end.
start_client()->
Pid2=spawn(allocator,loop,[]),
register(client, Pid2).
loop()->
receive
allocate->
allocate(),
loop();
{free,Resource}->
free(Resource),
loop();
stop->
true;
_->
loop()
end.
回家了,有空再詳細說明下這個例子吧。執行:
1> c(allocator).
{ok,allocator}
2> allocator:start([1,2,3,4,5,6]).
true
3> allocator:start_client().
true
4> client!allocate
.
allocate連接客戶端進程<0.37.0>
5> client!allocate.
allocate連接客戶端進程<0.37.0>
6> client!allocate.
allocate連接客戶端進程<0.37.0>
7> allocator:allocate().
連接客戶端進程<0.28.0>
{yes,4}
8> client!{free,1}.
{free,1}
9> client!{free,2}.
{free,2}
10> client!allocate.
allocate連接客戶端進程<0.37.0>
11> client!allocate.
allocate連接客戶端進程<0.37.0>
12> client!stop.
stop
13> allocator:allocate().
連接客戶端進程<0.28.0>
{yes,3}
14> allocator:allocate().
連接客戶端進程<0.28.0>
{yes,2}
15> allocator:allocate().
連接客戶端進程<0.28.0>
{yes,1}
16>
{ok,allocator}
2> allocator:start([1,2,3,4,5,6]).
true
3> allocator:start_client().
true
4> client!allocate
.
allocate連接客戶端進程<0.37.0>
5> client!allocate.
allocate連接客戶端進程<0.37.0>
6> client!allocate.
allocate連接客戶端進程<0.37.0>
7> allocator:allocate().
連接客戶端進程<0.28.0>
{yes,4}
8> client!{free,1}.
{free,1}
9> client!{free,2}.
{free,2}
10> client!allocate.
allocate連接客戶端進程<0.37.0>
11> client!allocate.
allocate連接客戶端進程<0.37.0>
12> client!stop.
stop
13> allocator:allocate().
連接客戶端進程<0.28.0>
{yes,3}
14> allocator:allocate().
連接客戶端進程<0.28.0>
{yes,2}
15> allocator:allocate().
連接客戶端進程<0.28.0>
{yes,1}
16>
文章轉自莊周夢蝶 ,原文發布時間5.17
最後更新:2017-05-17 14:02:05