erlang 四大 behaviour【转】

来自:http://www.jifuyi.com/tag/erlang/ 

erlang四大behaviour之一gen_server(转载) 

erlang程序设计里面有个设计原则就是把你的进程构造成树,把共用代码提出来,特定功能用自己的module实现,这也就是behaviour了,应用behaviour可以减少与本身事务无关的代码量,设计逻辑更加清晰。老纪边学习边记录吧。 

gen_server实现服务器/客户端模型,用于多个客户共用一个资源的这种情况。他由几个接口函数和几个回调函数组成(回调函数必须在你的module里定义)这些可以参考erlang的doc 

举个例子: 

Erlang代码   收藏代码
  1. -module(ch3).  %这是我们的回调模块,也是我们实现业务逻辑的模块  
  2.  -behaviour(gen_server).  % 说明我们应用gen_server这个behaviour  
  3.  -export([start_link/0]).  
  4.  -export([alloc/0, free/1]).  
  5. -export([init/1, handle_call/3, handle_cast/2]).  %gen_server 的导出函数  
  6. start_link() ->     
  7.  gen_server:start_link({local, ch3}, ch3, [], []).  
  8. alloc() ->    
  9.   gen_server:call(ch3, alloc).  
  10. free(Ch) ->     
  11.  gen_server:cast(ch3, {free, Ch}).  
  12. init(_Args) ->      
  13. {ok, channels()}.  
  14. handle_call(alloc, _From, Chs) ->   
  15.    {Ch, Chs2} = alloc(Chs),     
  16.  {reply, Ch, Chs2}.  
  17. handle_cast({free, Ch}, Chs) ->   
  18.    Chs2 = free(Ch, Chs),    
  19.   {noreply, Chs2}.  

gen_server:start_link的调用会生成一个服务器进程且连接到进程树,并调用我们的init函数。 gen_server:call(ch3, alloc)的调用导致对handle_call的调用,这是同步的。gen_server:cast(ch3, {free, Ch})的调用导致对handle_cast的调用,这是异步的。很简单。 

假如你不想把服务器进程挂入监控树的话,直接用gen_server:start启动进程,这是这个服务器进程就是一个普通进程了。 

gen_server的停止规则: 

以gen_server:start_link开始的连入监控树的 
一般情况不需要提供自己的停止函数,监控进程会自动处理,但是如果你想在gen_server进程中自己清理以下资源,那么就必须在init函数里调用process_flag(trap_exit, true)来捕获退出信号,这会导致调用terminate(shutdown, State)函数,所以你也必须实现这个函数 

以gen_server:start开始的单独gen_server 
终止他就比较简单,直接调用gen_server:cast(Name, stop),这会导致调用handle_cast(stop, State),它的实现里写入 {stop, normal, State}即可,它最终导致terminate(normal, State)的调用,你的清理工作就可以在这继续了。 



erlang越看越喜欢,但是要深入要走的路还很长 


erlang四大behaviour之二 - gen_fsm 
今天介绍erlang的一个非常重要的behaviour,就是gen_fsm-有限状态机,有限状态机的作用非常之多,比如文本解析,模式匹配、 游戏逻辑等等方面的处理都是它的强项,所以这个behaviour非常之重要 
1. 有限状态机 
有限状态机可以用下面这个公式来表达 

State(S) x Event(E) -> Actions(A), State(S') 
表示的就是在S状态时如果有事件E发生,那么执行动作A后把状态调整到S’。 

对于一个用gen_fsm行为实现的状态机来说,状态转变规则被写为符合如下规定的一系列Erlang函数: 
Java代码   收藏代码
  1. StateName( Event, StateData ) ->  
  2. .. code for actions here …  
  3. { next_state, StateName’, StateData’ }  



2. 一个例子 
erlang手册中用这个例子来解释的:开锁问题,有一个密码锁的门,它就可以看作一个状态机,初始状态门是锁着的,任何时候有人按一个密码键就会 产生一个事件,这个键值和前面的按键组合后与密码相比较,看是否正确,如果输入的密码顺序是对的,那么将门打开30秒,如果输入密码不完全,则等待下次按 钮按下,如果输入密码顺序是错的,则重新开始等待按键按下。 


Java代码   收藏代码
  1. -module(code_lock).  
  2. -behaviour(gen_fsm).  
  3. -export([start_link/1]).  
  4. -export([button/1]).  
  5. -export([init/1, locked/2, open/2]).  
  6. start_link(Code) ->  
  7.     gen_fsm:start_link({local, code_lock}, code_lock, Code, []).  
  8. button(Digit) ->  
  9.     gen_fsm:send_event(code_lock, {button, Digit}).  
  10. init(Code) ->  
  11.     {ok, locked, {[], Code}}.  
  12. locked({button, Digit}, {SoFar, Code}) ->  
  13.     case [Digit|SoFar] of  
  14.         Code ->  
  15.             do_unlock(),  
  16.             {next_state, open, {[], Code}, 3000};  
  17.         Incomplete when length(Incomplete)  
  18.             {next_state, locked, {Incomplete, Code}};  
  19.         _Wrong ->  
  20.             {next_state, locked, {[], Code}}  
  21.     end.  
  22. open(timeout, State) ->  
  23.     do_lock(),  
  24.     {next_state, locked, State}.  

这些代码下面解释 



3. 启动状态机 
在上一节提到的例子里,我们使用code_lock:start_link(Code)启动gen_fsm 

Java代码   收藏代码
  1. start_link(Code) ->  
  2.     gen_fsm:start_link({local, code_lock}, code_lock, Code, []).  

start_link调用gen_fsm:start_link/4,启动一个新的gen_fsm进程并连接。 
1)第一个参数{local, code_lock}指定名字,在本地注册为code_lock 
2)第二个参数code_lock是回调模块 
3)第三个参数Code是传递给回调模块init函数的参数,就是密码锁的密码 
4)第四个[]是状态机的选项 
如果进程注册成功,则新的gen_fsm进程调用code_lock:init(Code),返回{ok, StateName, StateData}。StateName是gen_fsm的初始状态,在这里返回的是locked,表示初始状态下门是锁着的,StateData是 gen_fsm的内部状态,在这里Statedata是当前的按键顺序(初始时为空)和正确的锁代码,是个列表 

Java代码   收藏代码
  1. init(Code) ->  
  2.     {ok, locked, {[], Code}}.  

注意gen_fsm:start_link是同步的,直到gen_fsm进程初始化并准备好开始接受请求时才会返回。加入gen_fsm是监控树的 一部分,那么gen_fsm:start_link必须被使用,也就是被一个监控者调用,gen_fsm:start则是启动单独的gen_fsm进程, 也就是gen_fsm不是监控树的一部分 


4. 事件通知 
使用gen_fsm:send_event/2来实现按建事件的通知 

Java代码   收藏代码
  1. button(Digit) ->  
  2.     gen_fsm:send_event(code_lock, {button, Digit}).  

code_lock是gen_fsm的名字,且必须用这个名字启动进程,{button, Digit}是发送的事件,事件是作为消息发送给gen_fsm的,当事件被接收到,gen_fsm就调用StateName(Event, StateData),它的返回值应该是{next_state, StateName1, StateData1}。StateName是当前状态名称,而StateName1是将转换到的下一状态名称,StateData1是 StateData的新值 

Java代码   收藏代码
  1. locked({button, Digit}, {SoFar, Code}) ->  
  2.     case [Digit|SoFar] of  
  3.         Code ->  
  4.             do_unlock(),  
  5.             {next_state, open, {[], Code}, 30000};  
  6.         Incomplete when length(Incomplete)  
  7.             {next_state, locked, {Incomplete, Code}};  
  8.         _Wrong ->  
  9.             {next_state, locked, {[], Code}};  
  10.     end.  
  11. open(timeout, State) ->  
  12.     do_lock(),  
  13.     {next_state, locked, State}.  

假如门是锁着的且按了一个按键,完整的按键序列和密码相比较,根据比较结果来决定门是打开(状态切到open)还是保持locked状态。 


5 超时 
假如输入的密码正确,门被打开,locked/2函数返回下面的序列 

Java代码   收藏代码
  1. {next_state, open, {[], Code}, 30000};  

30000表示超时30000毫秒,在30秒后,超时发生,调用StateName(timeout, StateData) ,门又重新锁上 

Java代码   收藏代码
  1. open(timeout, State) ->  
  2.     do_lock(),  
  3.     {next_state, locked, State}.  


6. 所有状态事件 
有时候一个事件可以到达gen_fsm进程的任何状态,取代用gen_fsm:send_event/2发送消息和写一段每个状态函数处理事件的代 码,这个消息我们可以用gen_fsm:send_all_state_event/2 发送,用Module:handle_event/3处理
 在gen_fsm的任何状态都有可能有事件到达。除了可以用gen_fsm:send_event/2发送消息,然后为每一个状态函数写一个子句来处理事件之外,还可以通过gen_fsm:send_all_state_event/2来发送消息,并通过Module:handle_event/3来处理。 

Java代码   收藏代码
  1. 1. -module(code_lock).  
  2. 2. …  
  3. 3. -export([stop/0]).  
  4. 4. …  
  5. 5. stop() ->  
  6. 6. gen_fsm:send_all_state_event(code_lock, stop).  
  7. 7. …  
  8. 8. handle_event(stop, _StateName, StateData) ->  
  9. 9. {stop, normal, StateData}.  




Java代码   收藏代码
  1. -module(code_lock).  
  2. ...  
  3. -export([stop/0]).  
  4. ...  
  5. stop() ->  
  6.     gen_fsm:send_all_state_event(code_lock, stop).  
  7. ...  
  8. handle_event(stop, _StateName, StateData) ->  
  9.     {stop, normal, StateData}.  

7. 停止 
假如gen_fsm是监控树的一部分,则不需要停止方法,gen_fsm会自动被监控者停止。如果需要在结束前清理数据,那么shutdown strategy必须为一个timeout,并且必须在gen_fsm的init方法里设置捕获exit信号,然后 
gen_fsm进程会调用callback方法terminate(shutdown, StateName, StateData) 

Java代码   收藏代码
  1. init(Args) ->  
  2.     ...,  
  3.     process_flag(trap_exit, true),  
  4.     ...,  
  5.     {ok, StateName, StateData}.  
  6. ...  
  7. terminate(shutdown, StateName, StateData) ->  
  8.     ..code for cleaning up here..  
  9.     ok.  


8. 独立gen_fsm进程 
加入gen_fsm不是监控树的一部分,stop函数可能有用,如: 

Java代码   收藏代码
  1. ...  
  2. -export([stop/0]).  
  3. ...  
  4. stop() ->  
  5.     gen_fsm:send_all_state_event(code_lock, stop).  
  6. ...  
  7. handle_event(stop, _StateName, StateData) ->  
  8.     {stop, normal, StateData}.  
  9. ...  
  10. terminate(normal, _StateName, _StateData) ->  
  11.     ok.  

回调函数处理stop事件并返回{stop, normal, StateData1},normal表示正常停止,StateData1为gen_fsm的新的StateData值,这将导致gen_fsm调用 terminate(normal, StateName, StateData1)然后自然的停止 



9. 处理其他信息 
收到的其他消息由handle_info(Info, StateName, StateData)处理,其他消息的一个例子就是exit消息,假如gen_fsm进程与其他进程link了并且trace了信号,就要处理exit消 息 

Java代码   收藏代码
  1. handle_info({'EXIT', Pid, Reason}, StateName, StateData) ->  
  2.     ..code to handle exits here..  
  3.     {next_state, StateName1, StateData1}.  



erlang四大behaviour之三-gen_event 

1. 事件处理规则 
在OTP中,事件管理器是一个事件可以发送到的命名对象,一个事件可以是一个错误、一个警告、或者一些要写入日志的信息 

在事件管理器中,有0个、一个或者多个事件处理器被安装,当事件管理器被一个事件通知时,这个事件将被安装在事件管理器中的事件处理器处理, 

事件管理器用一个进程实现,事件处理器用回调模块实现。事件管理器本质上维护一个{Module, State}列表,每一个Module为一个事件处理器,而State为事件处理器的内部状态。 

2. 例子 
事件处理器的回调模块把错误信息写入终端 

Java代码   收藏代码
  1. -module(terminal_logger).  
  2. -behaviour(gen_event).  
  3. -export([init/1, handle_event/2, terminate/2]).init(_Args) ->    {ok, []}.  
  4. handle_event(ErrorMsg, State) ->    io:format("***Error*** ~p~n", [ErrorMsg]),    {ok, State}.terminate(_Args, _State) ->    ok.  

事件处理器的回调模块把错误信息写入文件 

Java代码   收藏代码
  1. -module(file_logger).  
  2. -behaviour(gen_event).  
  3. -export([init/1, handle_event/2, terminate/2]).  
  4. init(File) ->    {ok, Fd} = file:open(File, read),    {ok, Fd}.  
  5. handle_event(ErrorMsg, Fd) ->    io:format(Fd, "***Error*** ~p~n", [ErrorMsg]),    {ok, Fd}.  
  6. terminate(_Args, Fd) ->    file:close(Fd).  



3. 启动事件管理器 
调用 

Java代码   收藏代码
  1. gen_event:start_link({local, error_man})  

启动管理器,这个函数生成并连接到一个新进程,参数{local, error_man}指定名称,在这个例子中,事件管理器被局部注册为error_man 

假如忽略名称,那么事件管理器不会被注册,它的PID将被使用。名称也可以是这种形式{global, Name},这样,事件管理器的名称是用global:register_name/2注册的。 

假如事件管理器是监控树的一部分,那么gen_event:start_link必须被使用,也就是被监控树启动,而gen_event:start启动单独的事件管理器,也就是事件管理器不是监控树的一部分。 



4. 添加事件处理器 
下面的例子显示怎样启动一个事件管理器和添加一个事件处理器 

Java代码   收藏代码
  1. 1> gen_event:start({local, error_man}).{ok,<0.31.0>}  
  2. 2> gen_event:add_handler(error_man, terminal_logger, []).ok  

Java代码   收藏代码
  1. gen_event:add_handler(error_man, terminal_logger, [])  
为error_man添加处理器terminal_logger,事件管理器调用terminal_logger:init([])这个回调函数, []是参数,init要返回一个{ok, State},State是事件处理器的内部状态 

Java代码   收藏代码
  1. init(_Args) ->    {ok, []}.  

这里,init不需要任何输入参数,对于terminal_logger,也没使用内部状态,对于file_logger,内部状态保存了打开的文件描述符 

Java代码   收藏代码
  1. init(File) ->    {ok, Fd} = file:open(File, read),    {ok, Fd}.  



5. 关于事件通知 


3> gen_event:notify(error_man, no_reply).***Error*** no_replyok 
error_man是事件管理器的名称,no_reply是事件,事件作为消息发送给事件管理器,当事件被收到时,事件管理器为每个安装的事件处理器按安装次序调用handle_event(Event, State),这个函数期待返回{ok, State1},State1是事件处理器的新状态。 

在terminal_logger中 

Java代码   收藏代码
  1. handle_event(ErrorMsg, State) ->    io:format("***Error*** ~p~n", [ErrorMsg]),    {ok, State}.  



在file_logger中 

Java代码   收藏代码
  1. handle_event(ErrorMsg, Fd) ->    io:format(Fd, "***Error*** ~p~n", [ErrorMsg]),    {ok, Fd}.  
  2.    


6. 删除一个事件处理器 
Java代码   收藏代码
  1. gen_event:delete_handler(error_man, terminal_logger, [])  
,这个函数向事件管理器error_man发送了一个消息,告诉他删除terminal_logger这个事件处理器,事件管理器将调用terminal_logger:terminate([], State),参数[]是delete_handler的第三个参数,terminate以init相反的方向调用,以完成清理工作,返回值被忽略。 

在terminal_logger中,没有清理动作 

Java代码   收藏代码
  1. terminate(_Args, _State) ->    ok.  



在file_logger中,文件描述符被关掉 

Java代码   收藏代码
  1. terminate(_Args, Fd) ->    file:close(Fd).  



7. 停止 
当事件管理器被停止,它给每个注册的事件处理器调用terminate/2的机会,就好像事件处理器被删除一样。如果事件管理器是监控树的一部分,不需要显示的停止事件管理器。当事件管理器作为单独进程使用时,则调用gen_event:stop(error_man). 



erlang四大behaviour之四-supervisor(转载) 
1. 监督规则 
一个监督者负责启动、停止、监控他的子进程。监督者的一个基本概念就是当必要的时候重启子进程保证它们的存活 

哪个子进程要重启和被监控是由一个子规程列表决定的,子进程按照列表中指定的顺序启动,并按相反的顺序终止

2. 实例 
监督者的回调模块 

Java代码   收藏代码
  1. -module(ch_sup).  
  2.  -behaviour(supervisor).  
  3.  -export([start_link/0]).  
  4.  -export([init/1]).  
  5.  start_link() ->      
  6. supervisor:start_link(ch_sup, []).  
  7.  init(_Args) ->     
  8.    {ok, {{one_for_one, 160},        
  9.     [{ch3, {ch3, start_link, []},        
  10.       permanent, brutal_kill, worker, [ch3]}]}}.  


one_for_one是重启策略 
1和60定义了最大重启频率 
{ch3, …}是子规程 

3. 重启策略 
one_for_one 

假如一个进程终止了,仅仅这个进程会被重启 

one_for_all 


假如一个进程停止了,所有其他子进程也要被停止,然后所有子进程,包括这个引发停止的子进程都被重启 
 



rest_for_one 

假如一个进程停止了,它后面的子进程,也就是以启动顺序来说这个被终止的进程后面的子进程都将被停止,然后他们又被启动。 
 




4. 最大启动频率 
监督者有一个内建机制限制在给定的时间间隔里的重启次数,这由子进程启动规程中的两个参数值决定,MaxR和MaxT,它们定义在回调函数init中 

Java代码   收藏代码
  1. init(...) ->     
  2.  {ok, {{RestartStrategy, MaxR, MaxT},         
  3.    [ChildSpec, ...]}}.  

如果在时间MaxT里重启次数大于MaxR ,监督者进程就停止它所有子进程,然后再终止自己。 

当监督者进程终止了,那么更高级别的监督者要采取些动作,它或者重启被终止的监督者或者停止自己 

这个重启机制的目的是预防一个进程因某种原因频繁的终止,然后简单的重启。 

5. 子规范 
下面的是类型定义 

Java代码   收藏代码
  1. {Id, StartFunc, Restart, Shutdown, Type, Modules}     
  2.  Id = term()     
  3.  StartFunc = {M, F, A}    
  4.       M = F = atom()     
  5.      A = [term()]   
  6.    Restart = permanent | transient | temporary    
  7.   Shutdown = brutal_kill | integer() >=0 | infinity     
  8.  Type = worker | supervisor      
  9. Modules = [Module] | dynamic        
  10.   Module = atom()  

Id用来内部标识子规范 
StartFunc是启动子进程时调用的函数,它将成为对supervisor:start_link, gen_server:start_link, gen_fsm:start_link or gen_event:start_link的调用 
Restart标识一个进程终止后将怎样重启,一个permanent 进程总会被重启;一个temporary 进程从不会被重启;一个transient 进程仅仅当是不正常的被终止后才重启,例如非normal得退出原因 
Shutdown 定义一个进程将怎样被终止,brutal_kill意味着子进程被exit(Child, kill)无条件的终止;一个整数值的超时时间意味着监督者告诉子进程通过调用exit(Child, shutdown)而被终止,然后等待一个返回的退出信号,假如在指定的时间里没有收到退出信号,那么子进程用exit(Child, kill)被无条件终止。 
Type指定子进程是supervisor还是worker 
Modules 是有一个元素的列表[Module],假如子进程是supervisor、gen_server 或 gen_fsm,那么Module 是回调模块的名称;假如子进程是gen_event,那么Modules 应该是dynamic 
例如:子规范用于启动一个服务器ch3 

Java代码   收藏代码
  1. {ch3, {ch3, start_link, []}, permanent, brutal_kill, worker, [ch3]}  

子规范用于启动一个事件管理器 

Java代码   收藏代码
  1. {error_man, {gen_event, start_link, [{local, error_man}]}, permanent, 5000, worker, dynamic}  



监督者然后根据子规程启动所有子进程,这个例子中是一个子进程ch3 

6. 启动supervisor 
像这样 

Java代码   收藏代码
  1. start_link() ->     
  2.  supervisor:start_link(ch_sup, []).  

启动 

监督者进程调用init 

Java代码   收藏代码
  1. init(_Args) ->      
  2. {ok, {{one_for_one, 160},         
  3.    [{ch3, {ch3, start_link, []},        
  4.       permanent, brutal_kill, worker, [ch3]}]}}.  

并期待init返回{ok, StartSpec} 

注意supervisor:start_link是同步的,它一直等到所有子进程都启动了才返回 

7. 添加子进程 
除静态监控树外,我们也可以通过supervisor:start_child(Sup, ChildSpec)向监督者动态添加子进程,Sup 是监督者的pid或名称,ChildSpec 是一个子规范。子进程用start_child/2来添加。注意:假如监督者死掉后重启,那么所有动态添加的子进程都不复存在 

8. 停止子进程 
任何静态动态添加的子进程都可以用supervisor:terminate_child(Sup, Id)来停止。一个停止子进程规范可以用supervisor:delete_child(Sup, Id)来删除。Sup是监督者的pid或名称,Id是子规范的id 

9. Simple-One-For-One 
监督者的simple_one_for_one启动策略是one_for_one的简版,所有子进程都是同一进程实例而被动态添加,下面是一个simple_one_for_one监督者的实例 

Java代码   收藏代码
  1. -module(simple_sup).  
  2. -behaviour(supervisor).  
  3. -export([start_link/0]).  
  4. -export([init/1]).  
  5. start_link() ->    supervisor:start_link(simple_sup, []).  
  6. init(_Args) ->   
  7.    {ok, {{simple_one_for_one, 01},     
  8.        [{call, {call, start_link, []},   
  9.            temporary, brutal_kill, worker, [call]}]}}.  

当启动时,监督者不启动任何子进程,取而代之的是所有子进程都通过调用supervisor:start_child(Sup, List)来动态添加,Sup 是监督者的pid或名称,List 是添加给子规范中指定参数列表term列表,如果启动函数是{M, F, A}这种形式,那么子进程通过调用apply(M, F, A++List)而被启动 

例如,给上面的例子添加一个子进程 

Java代码   收藏代码
  1. supervisor:start_child(Pid, [id1])  

那么子进程通过调用apply(call, start_link, []++[id1])而被启动,实际上就是
Java代码   收藏代码
  1. call:start_link(id1)  


10. 停止 
因为监控者是监控树的一部分,它自动被他的监督者停止,根据相应规范,它反序停止它的所有子进程,然后终止自己 

至此,四种behavour已经全部翻译完了,熟练应用他们是你构建高扩展、高容错、高并发应用的基础,努力吧!

你可能感兴趣的:(erlang)