Erlang笔记 -- 错误处理

本文深入探讨Erlang的错误处理,包括顺序编程中的异常产生、捕捉,如exit(), throw(), error()函数,以及并发编程中的异常处理机制,如连接、监视、错误信号的传播和处理。通过实例解析了try...catch语法和基本错误处理函数的使用,阐述了如何在Erlang中优雅地处理错误和异常。" 124953035,11813469,Spring缓存实战:@Cacheable、@CacheEvict与@CachePut解析,"['spring框架', '缓存技术', 'Redis']

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

顺序编程异常处理

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是一个原子(throwexiterror其中之一),它告诉我们异常错误是如何生成的。如果省略了ExceptionType,就会使用默认值throw
注意:Erlang运行时系统所检测到的内部错误总是带有error标签。
关键字after之后的代码是用来在FuncOrExpressionSeq结束后执行清理的。这段代码一定会被执行,哪怕有异常错误抛出也是如此。after区块的代码会在trycatch区块里的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)变成系统进程。
  • 连接
    进程可以互相连接。如果AB两个进程有连接,而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所示。虚线代表了连接。
图片来源于《Erlang程序设计 第二版》
为了创建连接,我们会调用基本函数link(Pid),它会在调用进程和Pid之间创建一个连接。因此,如果P1调用link(P3)P1P3之间就会建立连接。
P1调用了link(P3)P3又调用了link(P10),以此类推,最终得到了上图b所展示的情形。请注意,P1的连接组只有一个元素(P3),而P3的连接组有两个元素(P1P10)。

  1. 同步终止的进程组

通常,你希望创建能够同步终止的进程组。在论证系统行为的时候,这是一个非常有用的不变法则。当多个进程合作解决问题而某处出现问题时,有时候我们能进行恢复。但如果无法恢复,就会希望之前所做的一切事情都停止下来。它和事务(transaction)这个概念很像:进程要么做它们该做的事,要么全部被杀死。
假设我们有一些相互连接的进程而其中的某个进程挂了,比如下图a中的P9。图a展示了P9终止前各个进程是如何连接的。图b展示了P9崩溃且所有错误信号都处理完成后还剩下哪些进程。
图片来源于《Erlang程序设计 第二版》

P9终止时,一个错误信号被发送给进程P4P10。因为P4P10不是系统进程,所以也一起终止了,随后,错误信号被发送给与它们相连的所有进程。最后,错误信号扩散到了所有相连的进程,整个互连进程组都终止了。
如果P1P3P4P9P10里的任意进程终止,它们就会全部终止。

  1. 设立防火墙

有时候我们不希望相连的进程全部终止,而是想让系统里的错误停止扩散。下图对此进行了演示,里面所有的相连进程都会终止,一直到P3为止。
图片来源于《Erlang程序设计 第二版》

要实现这一点,P3可以执行process_flag(trap_exit, true)并转变成一个系统进程(意思是它可以捕捉退出信号)。如图b所示,它用双圆来表示。P9崩溃之后,错误的扩散会在P3处停止,因此P1P3不会终止。P3充当了一个防火墙,阻止错误扩散到系统里的其他进程中。

3. 监视

监视与连接类似,但是有几处明显的区别。

  • 监视是单向的。如果A监视BB挂了,就会向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. 实例
  1. 假设要创建若干工作进程来解决某个问题,它们分别执行函数F1, F2…。如果任何一个进程挂了,我们希望它们能全体终止。可以调用start([F1,F2, ...])来实现这一点。
start(Fs) ->
    spawn(fun() ->
        [spawn_link(F) || F <- Fs],
        receive
            after
                infinity -> true
        end
    end).

start(Fs)会分裂出一个进程,后者随即分裂并连接各个工作进程,然后无限期等待。如果任何一个工作进程挂了,它们就都会终止。

  1. 函数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程序设计 第二版》。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值