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}).

当没有使用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.

再看看结果,
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.

执行:

> 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

四、运行时失败
    一个运行时错误将导致进程的非正常终止,伴随着非正常终止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}给连接进程

if_clause - 同理,if语句缺少匹配分支

function_clause - 缺少匹配的函数,比如:

foo(1) ->
  yes;
foo(2) ->
  no.

如果我们调用foo(3),因为没有匹配的函数,将发出{‘EXIT‘, From, function_clause} 给连接的进程。

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.

创建的进程执行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>

六、未定义函数和未注册名字
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.

回家了,有空再详细说明下这个例子吧。执行:

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>

时间: 2024-08-29 16:33:45

Erlang入门(四)——错误处理和鲁棒性的相关文章

Erlang入门:构建application练习5(监督树)

1.前言 在<Erlang入门:构建application练习4(进程link的作用)>一文中, 提到了一个问题: 如果系统进程内部发生了错误而异常退出,将会出现什么情况? 在application中,像bank_center这样的系统进程,如果终止了,整个系统就无法正常运行,为了让bank_center意外终止时能自动重启,这里就要请Erlang的监督树出来帮忙了. 演示项目bank3目录结构: 2.编写监督树回调模块bank_sup.erl 监督树的回调模块只有一个函数init/1 %%

微服务(入门四):identityServer的简单使用(客户端授权)

IdentityServer简介(摘自Identity官网) IdentityServer是将符合规范的OpenID Connect和OAuth 2.0端点添加到任意ASP.NET核心应用程序的中间件,通常,您构建(或重新使用)一个包含登录和注销页面的应用程序(可能还包括同意,具体取决于您的需要),IdentityServer中间件向其添加必要的协议头,以便客户端应用程序可以使用这些标准协议与之对话. 托管应用程序可以像您希望的那样复杂,但我们通常建议通过只包含与身份验证相关的UI来尽可能地保持

Java使用Protocol Buffers入门四步骤

Protocol Buffers(简称protobuf)是谷歌的一项技术,用于将结构化的数据序列化.反序列化,经常用于网络传输. 这货实际上类似于XML生成和解析,但protobuf的效率高于XML,不过protobuf生成的是字节码,可读性比XML差.类似的还有json.Java的Serializable等. protobuf支持各种语言.本文以Java为例,简单介绍protobuf如何使用.其他语言使用方法类似. 首先需要下载: http://download.csdn.net/downlo

Thinkphp入门 四 —布局、缓存、系统变量 (48)

原文:Thinkphp入门 四 -布局.缓存.系统变量 (48) [控制器操作方法参数设置] http://网址/index.php/控制器/操作方法 [页面跳转] [变量调节器] Smarty变量调节器 TP变量调节器:普通的php函数 (count  strlen   str_replace) 定义:前者的输出,是后者的输入 [子模板包含] 当前模块彼此包含 <include  file=”模板名称”  /> [使用布局layout] 1. 开启布局,配置变量信息config.php 2.

Redbean:入门(四) - 反射机制 以及 事务

<?php //引入rb入口文件 include_once 'rb.php'; //定义dsn以及相关的数据 $dsn = 'mysql:host=localhost;dbname=hwibs_model'; $user = 'root'; $pass = ''; $table = 'link'; //链接数据库 R::setup($dsn,$user,$pass); //链接数据表 $handler = R::dispense($table); //inspect::反射表,将表的字段结构返回

[WebGL入门]四,渲染准备

注:文章译自http://wgld.org/,原作者杉本雅広(doxas),文章中如果有我的额外说明,我会加上[lufy:],另外,鄙人webgl研究还不够深入,一些专业词语,如果翻译有误,欢迎大家指正. 必须准备的东西 上次介绍了3D绘图的基础知识.讲了一下由Z坐标的不同决定的两种坐标系,以及坐标变换的种类.这一次,说一说实际WebGL绘图的时候必须准备的东西. 首先,HTML,javascript相关的基础知识就不解释了.如果,有不明白的单词或概念的话,请自己查一下.我是认为你有一定的HTM

【原创】NIO框架入门(四):Android与MINA2、Netty4的跨平台UDP双向通信实战

概述 本文演示的是一个Android客户端程序,通过UDP协议与两个典型的NIO框架服务端,实现跨平台双向通信的完整Demo. 当前由于NIO框架的流行,使得开发大并发.高性能的互联网服务端成为可能.这其中最流行的无非就是MINA和Netty了,MINA目前的主要版本是MINA2.而Netty的主要版本是Netty3和Netty4(Netty5已经被取消开发了:详见此文). 本文中,服务端将分别用MINA2和Netty4进行实现,但在你实际的项目中服务端实现只需选其一就行了.本文中的Demo同时

AppleWatch开发入门四——Table视图的应用

AppleWatch开发入门四--Table视图的应用 一.Watch上的Table WatchOS中的TableView和iOS中的TableView还是有很大的区别,在开发之前,首先我们应该明白WatchOS中的Table有哪些局限性和特点.下面几点是我总结WatchOS中Table的特殊之处: 1.Table只有行的概念,没有分区的概念,没有头尾视图的概念. 2.可以通过创建多个Table,来实现分区的效果. 3.因为Watch上是通过Gruop进行布局适应的,所以没有行高等设置. 4.T

DevExpress XtraReports 入门四 创建 Web 报表

原文:DevExpress XtraReports 入门四 创建 Web 报表 本文只是为了帮助初次接触或是需要DevExpress XtraReports报表的人群使用的,为了帮助更多的人不会像我这样浪费时间才写的这篇文章,高手不想的看请路过 本文内容来DevExpress XtraReports帮助文档,如看过类似的请略过. 废话少说 开始正事 一.创建 Web 报表并绑定数据  启动 MS Visual Studio (2005.2008.或 2010). 新建一个 ASP.NET Web

转 Python爬虫入门四之Urllib库的高级用法

静觅 » Python爬虫入门四之Urllib库的高级用法 1.设置Headers 有些网站不会同意程序直接用上面的方式进行访问,如果识别有问题,那么站点根本不会响应,所以为了完全模拟浏览器的工作,我们需要设置一些Headers 的属性. 首先,打开我们的浏览器,调试浏览器F12,我用的是Chrome,打开网络监听,示意如下,比如知乎,点登录之后,我们会发现登陆之后界面都变化了,出现一个新的界面,实质上这个页面包含了许许多多的内容,这些内容也不是一次性就加载完成的,实质上是执行了好多次请求,一般