Erlang try ... catch 和 case catch ... of 的区别

最近在做 DBMXS(Extra Services provided by Database Middleware,数据库中间件) 项目的时候,有个场景需要捕捉进程退出的异常消息。突然很好奇捕捉异常的两个方式 try … catch 和 case catch … of 有什么区别。

于是在 google 上搜索一番之后,找到了答案。首先摘抄 Richard Carlsson 在邮件组中的一段话:

Finally, this is also another reason to rewrite old occurrences of 
'catch Expr' into 'try Expr catch ... end', because it basically works 
like this:

   try Expr
   catch
     throw:Term -> Term;
      exit:Term -> {'EXIT', Term};
     error:Term -> {'EXIT', {Term, erlang:get_stacktrace()}}
   end

so what happens if you use one of the following old idioms?:

   ...
   catch foo(...),  % for side effect, ignore the result
   ...

or

   case catch foo(...) of
     {'EXIT', Reason} -> ...;
     Result -> ...
   end

Well, when the exception type is 'error', the catch will build a result 
containing the symbolic stack trace, and this will then in the first 
case be immediately discarded, or in the second case matched on and then 
possibly discarded later. Whereas if you use try/catch, you can ensure 
that no stack trace is constructed at all to begin with.

简单说就是如果抛出的异常类型是 error,在用 catch 这个关键字直接捕捉的时候,是默认要生成 stack trace 信息的。这个是对性能有一定损耗的。因此对性能敏感的函数逻辑里,建议使用 try … catch 来代替直接 catch 的方式来捕捉异常。

以下做个实验。首先写一个测试 module:

-module(test).

-export([loop/4, try_catch_test/1, try_catch_test/2, catch_test/1, catch_test/2]).                                                                                              

loop(0, _, _, _) -> ok;
loop(Times, M, F, A) ->
    _ = apply(M, F, A),
    loop(Times - 1, M, F, A).

try_catch_test(Type) -> try_catch_test(Type, false).

try_catch_test(Type, TestLoop) ->
    try
        make_an_exception(Type)
    catch
        Type:Reason ->
            case TestLoop of
                true -> ok;
                false -> io:format("try .. catch block caught exception of ~p: ~p~n", [Type, Reason])
            end
    end.

catch_test(Type) -> catch_test(Type, false).

catch_test(Type, TestLoop) ->
    case catch make_an_exception(Type) of
        nothing_wrong -> nothing_wrong;
        Exception ->
            case TestLoop of
                true -> ok;
                false -> io:format("catch block caught exception of ~p~n", [Exception])
            end
    end.


make_an_exception(Type) ->
    case Type of
        throw -> erlang:throw(ladies);
        error -> erlang:error(ladies);
        exit -> erlang:exit(ladies);
        _ -> nothing_wrong
    end.

然后,分别进行两个实验过程:查看两种异常信息捕捉的实际效果差异和性能差异。

  • 实际效果差异

1> c(test).
{ok,test}
2> test:try_catch_test(throw).
try .. catch block caught exception of throw: ladies
ok
3> test:try_catch_test(error).
try .. catch block caught exception of error: ladies
ok
4> test:try_catch_test(exit). 
try .. catch block caught exception of exit: ladies
ok
5>                            
5> 
5> 
5> 
5> 
5> test:catch_test(throw).
catch block caught exception of ladies
ok
6> test:catch_test(error).
catch block caught exception of {'EXIT',
                                 {ladies,
                                  [{test,make_an_exception,1,
                                    [{file,"test.erl"},{line,23}]},
                                   {test,catch_test,1,
                                    [{file,"test.erl"},{line,14}]},
                                   {erl_eval,do_apply,6,
                                    [{file,"erl_eval.erl"},{line,674}]},
                                   {shell,exprs,7,
                                    [{file,"shell.erl"},{line,687}]},
                                   {shell,eval_exprs,7,
                                    [{file,"shell.erl"},{line,642}]},
                                   {shell,eval_loop,3,
                                    [{file,"shell.erl"},{line,627}]}]}}
ok
7> test:catch_test(exit). 
catch block caught exception of {'EXIT',ladies}
ok
8>

在使用 try … catch 去捕捉 throw、error 和 exit 三种异常的时候,都只是捕捉到异常 tag 和异常消息,没有 stack trace。而在使用 catch 直接捕捉异常的时候,捕捉 throw 和 exit 都没有 stack trace,但是在捕捉 error 异常的时候,就同时生成了 stack trace。

  • 性能差异

9> timer:tc(test, loop, [10000000, test, try_catch_test, [throw, true]]).
{1661010,ok}
10> timer:tc(test, loop, [10000000, test, try_catch_test, [error, true]]).
{1612950,ok}
11> timer:tc(test, loop, [10000000, test, try_catch_test, [exit, true]]). 
{1628293,ok}
12> timer:tc(test, loop, [10000000, test, catch_test, [throw, true]]).   
{1592246,ok}
13> timer:tc(test, loop, [10000000, test, catch_test, [error, true]]).
{9939126,ok}
14> timer:tc(test, loop, [10000000, test, catch_test, [exit, true]]). 
{1685309,ok}
15>

将以上六种场景分别进行 10,000,000 次调用,然后用 timer:tc/3 进行耗时统计。发现在直接用 catch 去捕捉 error 异常的时候,耗时将近 10 秒。而用 try … catch 方式捕捉 error 异常才花了 1.6 秒左右。可见直接用 catch 去捕捉 error 异常的时候隐式生成 stack trace 对性能的损耗之大。

总结:尽量用 try … catch 捕捉异常,如果真的需要获取 stack trace,可以在捕捉异常后手动执行 erlang:get_stacktrace/0 来获取。

点赞