顺序编程异常处理
1. 异常产生
异常错误发生于系统遇到内部错误时,或者通过在代码里显式调用throw(Exception)
、exit(Exception)
或error(Exception)
触发。
会触发异常错误的典型内部错误有模式匹配错误(没有一个函数子句能成功匹配),用错误类型的参数调用内置函数(比如用一个整数作为参数调用atom_to_list
),以及用带有错误值的参数调用内置函数(比如试图让某个数字除以0)。
可以通过调用下面的某个内置函数来显式生成一个错误。
- exit(Why)
当你确实想要终止当前进程时就用它。如果这个异常错误没有被捕捉到,信号{'EXIT',Pid,Why}
就会被广播给当前进程链接的所有进程。 - throw(Why)
这个函数的作用是抛出一个调用者可能想要捕捉的异常错误。在这种情况下,我们注明了被调用函数可能会抛出这个异常错误。 - error(Why)
这个函数的作用是指示“崩溃性错误”,它与系统内部生成的错误差不多。
2. 异常捕捉
Erlang有两种方法来捕捉异常错误。第一种是把抛出异常错误的调用函数封装在一个try...catch
表达式里,另一种是把调用封装在一个catch
表达式里。
- try…catch
try FuncOrExpressionSeq of
Pattern1 [when Guard1] -> Expressions1;
Pattern2 [when Guard2] -> Expressions2;
...
catch
ExceptionType1:ExPattern1 [when ExGuard1] -> ExExpressions1;
ExceptionType2:ExPattern2 [when ExGuard2] -> ExExpressions2;
...
after
AfterExpressions
end
try...catch
的工作方式如下:首先执行FuncOrExpessionSeq
。如果执行过程没有抛出异常错误,那么函数的返回值就会与Pattern1
(以及可选的关卡Guard1
)、Pattern2
等模式进行匹配,直到匹配成功。如果能匹配,那么整个try...catch
的值就通过执行匹配模式之后的表达式序列得出。
如果FuncOrExpressionSeq
在执行中抛出了异常错误,那么ExPattern1
等捕捉模式就会与它进行匹配,找出应该执行哪一段表达式序列。ExceptionType
是一个原子(throw
、exit
和error
其中之一),它告诉我们异常错误是如何生成的。如果省略了ExceptionType
,就会使用默认值throw
。
注意:Erlang运行时系统所检测到的内部错误总是带有error
标签。
关键字after
之后的代码是用来在FuncOrExpressionSeq
结束后执行清理的。这段代码一定会被执行,哪怕有异常错误抛出也是如此。after
区块的代码会在try
或catch
区块里的Expressions
代码完成后立即运行。AfterExpressions
的返回值会被丢弃。
try...catch
可简写如下,after
部分可省略:
try F
catch
...
end
相当于
try F
Val -> Val
catch
...
end
- catch
异常错误如果发生在catch
语句里,就会被转换成一个描述此错误的{'EXIT', ...}
元组。
3. 实例
%% try_test.erl
generate_exception(1) -> a;
generate_exception(2) -> throw(a);
generate_exception(3) -> exit(a);
generate_exception(4) -> {'Exit', a};
generate_exception(5) -> error(a).
demo1() ->
[catcher(I) || I <- [1, 2, 3, 4, 5]].
catcher(N) ->
try generate_exception(N) of
Val -> {N, normal, Val}
catch
throw:X -> {N, caught, thrown, X};
exit:X -> {N, caught, exited, X};
error:X -> {N, caught, error, X}
end.
demo2() ->
[{I, {catch generate_exception(I)}} || I <- [1, 2, 3, 4, 5]].
> try_test:demo1().
[{1,normal,a},
{2,caught,thrown,a},
{3,caught,exited,a},
{4,normal,{'Exit',a}},
{5,caught,error,a}]
> try_test:demo2().
[{1,{a}},
{2,{a}},
{3,{{'EXIT',a}}},
{4,{{'Exit',a}}},
{5,
{{'EXIT',{a,[{try_test,generate_exception,1,
[{file,"try_test.erl"},{line,8}]},
{try_test,'-demo2/0-lc$^0/1-0-',1,
[{file,"try_test.erl"},{line,23}]},
{try_test,'-demo2/0-lc$^0/1-0-',1,
[{file,"try_test.erl"},{line,23}]},
{erl_eval,do_apply,6,[{file,"erl_eval.erl"},{line,674}]},
{shell,exprs,7,[{file,"shell.erl"},{line,686}]},
{shell,eval_exprs,7,[{file,"shell.erl"},{line,641}]},
{shell,eval_loop,3,[{file,"shell.erl"},{line,626}]}]}}}}]
try...catch
方法概括了信息,catch
则提供了详细的栈跟踪信息。
1.内置函数error/1
的一种用途是改进错误消息的质量。
sqrt(X) when X < 0 ->
error({negativeArgument, X});
sqrt(X) ->
math:sqrt(X).
> math:sqrt(-1).
** exception error: an error occurred when evaluating an arithmetic expression
in function math:sqrt/1
called as math:sqrt(-1)
> try_test:sqrt(-1).
** exception error: {negativeArgument,-1}
in function try_test:sqrt/1 (try_test.erl, line 26)
2.捕捉到一个异常错误后,可以调用erlang:get_stacktrace()
来找到最近的栈跟踪信息。
并发编程异常处理
1. 错误处理的术语
- 进程
进程有两种:普通进程和系统进程。spawn
创建的是普通进程。普通进程可以通过执行内置函数process_flag(trap_exit, true)
变成系统进程。 - 连接
进程可以互相连接。如果A
和B
两个进程有连接,而A
出于某种原因终止了,就会向B
发送一个错误信号,反之亦然。 - 连接组
进程P
的连接组是指与P
相连的一组进程。 - 监视
监视和连接很相似,但它是单向的。如果A
监视B
,而B
出于某种原因终止了,就会向A
发送一个“宕机”消息,但反过来就不行了。 - 消息和错误信息
进程协作的方式是交换消息或错误信号。消息是通过基本函数send
发送的,错误信号则是进程崩溃或进程终止时自动发送的。错误信号会发送给终止进程的连接组。 - 错误信号的接收
当系统进程收到错误信号时,该信号会被转换成{'EXIT', Pid, Why}
形式的消息。Pid
是终止进程的标识,Why
是终止原因。如果进程是无错误终止,Why
就会是原子normal
,否则Why
会是错误的描述。
当普通进程收到错误信号时,如果退出原因不是normal
,该进程就会终止。当它终止时,同样会向它的连接组广播一个退出信号。 - 显示错误信号
任何执行exit(Why)
的进程都会终止(如果代码不是在 catch 或 try 的范围内执行的话),并向它的连接组广播一个带有原因Why
的退出信号。
进程可以通过执行exit(Pid, Why)
来发送一个“虚假”的错误信号。在这种情况下,Pid
会收到一个带有原因Why
的退出信号。调用exit/2
的进程则不会终止。 - 不可捕捉的退出信号
系统进程收到摧毁信号(kill signal)
时会终止。摧毁信号是通过调用exit(Pid, kill)
生成的。这种信号会绕过常规的错误信号处理机制,不会被转换成消息。摧毁信号只应该用在其他错误处理机制无法终止的顽固进程上。
2. 创建连接
假设有一组互不相关的进程,如下图a所示。虚线代表了连接。
为了创建连接,我们会调用基本函数link(Pid)
,它会在调用进程和Pid
之间创建一个连接。因此,如果P1
调用link(P3)
,P1
和P3
之间就会建立连接。
P1
调用了link(P3)
,P3
又调用了link(P10)
,以此类推,最终得到了上图b所展示的情形。请注意,P1
的连接组只有一个元素(P3
),而P3
的连接组有两个元素(P1
和P10
)。
- 同步终止的进程组
通常,你希望创建能够同步终止的进程组。在论证系统行为的时候,这是一个非常有用的不变法则。当多个进程合作解决问题而某处出现问题时,有时候我们能进行恢复。但如果无法恢复,就会希望之前所做的一切事情都停止下来。它和事务(transaction)这个概念很像:进程要么做它们该做的事,要么全部被杀死。
假设我们有一些相互连接的进程而其中的某个进程挂了,比如下图a中的P9
。图a展示了P9
终止前各个进程是如何连接的。图b展示了P9
崩溃且所有错误信号都处理完成后还剩下哪些进程。
当P9
终止时,一个错误信号被发送给进程P4
和P10
。因为P4
和P10
不是系统进程,所以也一起终止了,随后,错误信号被发送给与它们相连的所有进程。最后,错误信号扩散到了所有相连的进程,整个互连进程组都终止了。
如果P1
、P3
、P4
、P9
或P10
里的任意进程终止,它们就会全部终止。
- 设立防火墙
有时候我们不希望相连的进程全部终止,而是想让系统里的错误停止扩散。下图对此进行了演示,里面所有的相连进程都会终止,一直到P3
为止。
要实现这一点,P3
可以执行process_flag(trap_exit, true)
并转变成一个系统进程(意思是它可以捕捉退出信号)。如图b所示,它用双圆来表示。P9
崩溃之后,错误的扩散会在P3
处停止,因此P1
和P3
不会终止。P3
充当了一个防火墙,阻止错误扩散到系统里的其他进程中。
3. 监视
监视与连接类似,但是有几处明显的区别。
- 监视是单向的。如果
A
监视B
而B
挂了,就会向A
发送一个退出消息,反过来则不会如此。 - 如果被监视的进程挂了,就会向监视进程发送一个“宕机”消息,而不是退出信号。这就意味着监视进程即使不是系统进程也能够处理错误。当你想要不对称的错误处理时,可以使用监视,对称的错误处理则适合使用连接。监视通常会被服务器用来监视客户端的行为。
4. 基本错误处理函数
下列基本函数被用来操作连接和监视,以及捕捉和发送退出信号:
-
-spec spawn_link(Fun) -> Pid
-spec spawn_link(Mod, Fun, Args) -> Pid
它们的行为类似于spawn(Fun)
和spawn(Mod,Func,Args)
,同时还会在父子进程之间创建连接。 -
-spec spawn_monitor(Fun) -> {Pid, Ref}
-spec spawn_monitor(Mod, Fun, Args) -> {Pid, Ref}
它与spawn_link
相似,但创建的是监视而非连接。Pid
是新创建进程的进程标识符,Ref
是该进程的引用。如果这个进程因为Why
的原因终止了,消息{'DOWN',Ref,process,Pid,Why}
就会被发往父进程。 -
-spec process_flag(trap_exit, true)
它会把当前进程转变成系统进程。系统进程是一种能接收和处理错误信号的进程。 -
-spec link(Pid) -> true
它会创建一个与进程Pid
的连接。连接是双向的。如果进程A
执行了link(B)
,就会与B
相连。实际效果就和B
执行link(A)
一样。
如果进程Pid
不存在,就会抛出一个noproc
退出异常。
如果执行link(B)
时A
已经连接了B
,这个调用就会被忽略。 -
-spec unlink(Pid) -> true
它会移除当前进程和进程Pid
之间的所有连接。 -
-spec erlang:monitor(process, Item) -> Ref
它会设立一个监视。Item
可以是进程的Pid
,也可以是它的注册名称。 -
-spec demonitor(Ref) -> true
它会移除以Ref
作为引用的监视。 -
-spec exit(Why) -> none
它会使当前进程因为Why
的原因终止。如果执行这一语句的子句不在catch
语句的范围内,此进程就会向当前连接的所有进程广播一个带有参数Why
的退出信号。它还会向所有监视它的进程广播一个DOWN
消息。 -
-spec exit(Pid, Why) -> true
它会向进程Pid
发送一个带有原因Why
的退出信号。执行这个内置函数的进程本身不会终止。它可以用于伪造退出信号。
5. 实例
- 假设要创建若干工作进程来解决某个问题,它们分别执行函数
F1
,F2
…。如果任何一个进程挂了,我们希望它们能全体终止。可以调用start([F1,F2, ...])
来实现这一点。
start(Fs) ->
spawn(fun() ->
[spawn_link(F) || F <- Fs],
receive
after
infinity -> true
end
end).
start(Fs)
会分裂出一个进程,后者随即分裂并连接各个工作进程,然后无限期等待。如果任何一个工作进程挂了,它们就都会终止。
- 函数
on_exit(Pid, Fun)
会监视进程Pid
,如果它因为原因Why
退出了,就会执行Fun(Why)
。
%% lib_misc.erl
-module(mlib_misc).
-export([on_exit/2]).
on_exit(Pid, Fun) ->
spawn(
fun() ->
Ref = monitor(process, Pid),
receive
{'DOWN', Ref, process, Pid, Why} ->
Fun(Why)
end
end
).
%% list2atom
-module(list2atom).
-export([start/0, loop/0]).
start() ->
spawn(list2atom, loop, []).
loop() ->
receive
X -> io:format("The result is ~p~n", [list_to_atom(X)])
end,
loop().
Eshell V8.3 (abort with ^G)
1> c(mlib_misc).
{ok,mlib_misc}
2> c(list2atom).
{ok,list2atom}
3> Pid = list2atom:start().
<0.67.0>
4> mlib_misc:on_exit(Pid, fun(Why) -> io:format("exited for ~p~n", [Why]) end).
<0.69.0>
5> Pid ! [1,2,3].
[1,2,3]
The result is '\001\002\003'
6> Pid ! hello.
hello
exited for {badarg,[{erlang,list_to_atom,[hello],[]},
{list2atom,loop,0,[{file,"list2atom.erl"},{line,10}]}]}
7>
=ERROR REPORT==== 16-Oct-2020::11:04:22 ===
Error in process <0.67.0> with exit value:
{badarg,[{erlang,list_to_atom,[hello],[]},
{list2atom,loop,0,[{file,"list2atom.erl"},{line,10}]}]}
7>
注:本博客为通过《Erlang程序设计 第二版》学习Erlang时所做的笔记。学习更详细的内容,建议直接阅读《Erlang程序设计 第二版》。