【译】OTP Design Principles之Gen_Fsm Behaviour
OTP Design Principle中的gen_fsm,翻译了一下,希望对大家有用。:)翻译的不好,请大家结合原文来看。
1.3 Gem_Fsm Behaviour
本章应该结合gen_fsm(3)来阅读,在gen_fsm(3)里面有所有接口函数和回调函数的详细说明。
1.3.1 有限状态机
一个有限状态机,FSM,可以用一个关系式来描述:
State(S) x Event(E) -> Actions(A), State(S’)
这些关系解释如下:
如果我们处在状态S并且事件E发生了,那么,我们需要执行动作A,并且转变到状态S’。
对于一个用gen_fsm行为实现的状态机来说,状态转变规则被写为符合如下规定的一系列Erlang函数:
StateName( Event, StateData ) ->
.. code for actions here …
{ next_state, StateName’, StateData’ }
1.3.2 例子
一个带有密码锁的们可以被视作一个FSM。初始状态下,门是锁着的。任何时候,一个人按下一个按钮,就会产生一个事件。取决于之前哪些按钮被按下,现在的序列可能是正确的,不完整的,或者错误的。
如果是正确的,那么门会打开30秒(30000ms)。如果是不完整的,那么我们等待另一个按钮被按下。如果是错误的,那么我们从头开始,等待一个新的按钮序列。
使用gen_fsm实现密码锁的状态机,取决于如下的回调模块:
- -module(code_lock).
- -behaviour(gen_fsm).
- -export([start_link/1]).
- -export([button/1]).
- -export([init/1, locked/2, open/2]).
- start_link(Code) ->
- gen_fsm:start_link({local, code_lock}, code_lock, Code, []).
- button(Digit) ->
- gen_fsm:send_event(code_lock, {button, Digit}).
- init(Code) ->
- {ok, locked, {[], Code}}.
- locked({button, Digit}, {SoFar, Code}) ->
- case [Digit|SoFar] of
- Code ->
- do_unlock(),
- {next_state, open, {[], Code}, 3000};
- Incomplete when length(Incomplete)<length(Code) ->
- {next_state, locked, {Incomplete, Code}};
- _Wrong ->
- {next_state, locked, {[], Code}}
- end.
- open(timeout, State) ->
- do_lock(),
- {next_state, locked, State}.
该代码将会在如下的章节里面进行解释。
1.3.3 启动一个Gen_Fsm
在上一节的例子里面,通过调用code_lock:start_link(Code)来启动gen_fsm:
start_link(Code) ->
gen_fsm:start_link({local, code_lock}, code_lock, Code, []).
Start_link调用函数gen_fsm:start_link/4。该函数产生并连接到一个新的进程,一个gen_fsm.
- l 第一个参数{ local, code_lock }指明了名字。在这个例子里面,gen_fsm会被做为code_lock在本地注册。
如果名字省略了,那么gen_fsm没有被注册。这样就必须使用它的pid。名字也可以这样写{global, Name },这样,gen_fsm就是使用global_register_name/2来注册了。
- l 第二个参数,code_lock,是回调模块的名字,也就是回调函数所位于的模块。
在这个例子里面,接口函数(start_link 和 button)是位于和回调函数(init, locked和open)相同的模块里面。这是正常的,较好的编程实践,让对应一个进程的代码包含在一个模块里面。
- l 第三个参数,Code,就是在调用回调函数init的时候所传递的一项。这里,init得到锁的正确代码做为内部数据(indata)。
- l 第四个参数,[],是一个选项的列表。参见gen_fsm(3)来找到可用的选项。
如果名字注册成功,新的gen_fsm进程会调用回调函数code_lock:init(Code).这个函数要求返回{ok, StateName, StateData},其中StateName是gen_fsm初始状态的名字。在这个例子里面是locked,假设门一开始是锁着的。StateData是gen_fsm的内部状态。(对于gen_fsm来说,内部状态一般是指‘状态数据’(state data),以区别于状态机的状态)。在这个例子里面,状态数据(state data)是到目前为止的按钮的顺序(开始的时候为空)和锁的正确密码顺序。
- Init( Code ) ->
- { ok, locked, {[], Code} }.
注意,gen_fsm:start_link是同步的。直到gen_fsm已经完成初始化并且准备好接收通知的时候才返回。
如果gen_fsm是一个监控树的一部分的话,必须使用gen_fsm:start_link。即,gen_fsm是由一个监控者(supervisor)来启动。有另外一个函数gen_fsm:start来启动一个独立的gen_fsm,即,当gen_fsm不是监控树的一部分的时候。
1.3.4 事件通知
通知密码锁一个按钮被按下的事件是通过使用gen_fsm:send_event/2来完成的:
- Button( Digit ) ->
- Gen_fsm:send_event( code_lock, { button, Digit } ).
Code_lock是gen_fsm的名字并且必须与启动时候所使用的名字一致。{button,Digit}是实际的事件。
事件被做为消息发送给gen_fsm。当gen_gsm收到这个事件的时候,就会调用StateName( Event, StateData ),该函数需要返回一个元组{ next_state, StateName1, StateData1}. StateName是当前状态的名字,而StateName1是要转到的下一个状态的名字。StateData1是gen_gsm的状态数据的新的值。
- locked({button, Digit}, {SoFar, Code}) ->
- case [Digit|SoFar] of
- code ->
- do_unlock(),
- {next_state, open, {[], Code}, 3000};
- Incomplete when length(Incomplete)<length(Code) ->
- {next_state, locked, {Incomplete, Code}};
- _Wrong ->
- {next_state, locked, {[], Code}}
- end.
- open(timeout, State) ->
- do_lock(),
- {next_state, locked, State}.
如果门是锁着的,并且一个按钮被按下了,那么,到目前位置所有的按钮顺序就会与密码锁正确的密码顺序进行比较。根据比较的结果,门或者是打开了并且gen_gsm进入打开的状态,或者是仍然是锁着的。
1.3.5 超时
当一个正确的按钮顺序被按下之后,门会被打开,下面的元组就会从函数locked/2返回:
{ next_state, open, { [], Code }, 3000 };
30000是一个以毫秒为单位的超时。30000ms,也就是30秒后,一个超时发生了。然后StateName( timeout,StateData)就会被调用。在这个例子里面,当门打开30秒后就会发生超时。之后,门又会被锁上:
- Open( timeout, State ) ->
- Do_lock(),
- { next_state, locked, State }.
1.3.6 所有的状态事件
在gen_fsm的任何状态都有可能有事件到达。除了可以用gen_fsm:send_event/2发送消息,然后为每一个状态函数写一个子句来处理事件之外,还可以通过gen_fsm:send_all_state_event/2来发送消息,并通过Module:handle_event/3来处理。
- -module(code_lock).
- …
- -export([stop/0]).
- …
- stop() ->
- gen_fsm:send_all_state_event(code_lock, stop).
- …
- handle_event(stop, _StateName, StateData) ->
- {stop, normal, StateData}.
1.3.7 停止
在监控树里面
如果gen_gsm是监控树的一部分,那么不需要stop函数。Gen_fsm会自动的被它的监控者停止。具体如何实现是通过监控者里面的shutdown策略集来定义(参见22页)。
如果需要在停止之前进行清除操作,那么shutdown策略必须是一个超时值,并且,gen_fsm必须在init函数里面设置捕捉推出信号。当被命令关掉的时候,gen_fsm会调用回调函数terminate(shutdown, StateName, StateData).
- init(Args) ->
- …,
- process_flag(trap_exit, true),
- …,
- {ok, StateName, StateData}.
- …
- terminate(shutdown, StateName, StateData) ->
- ..code for cleaning up here..
- ok.
独立的Gen_Fsms
如果gen_fsm不是监控树的一部分,那么一个停止函数是有用的,例如:
- …
- -export([stop/0]).
- …
- stop() ->
- gen_fsm:send_all_state_event(code_lock, stop).
- …
- handle_event(stop, _StateName, StateData) ->
- {stop, normal, StateData}.
- …
- terminate(normal, _StateName, _StateData) ->
- ok.
处理stop时间的回调函数返回一个元组{stop,normal,StateData1},这里的normal标明这是一个正常的终止,并且StateData1是gen_fsm的一个新的状态数据值。这会使gen_fsm去调用terminate(normal, StateName, StateData1 ),然后优雅的终止。
1.3.8 处理其他消息
如果gen_fsm需要接收时间之外的消息,那么回调函数handle_info(Info, StateName, StateData)必须被实现,来处理这些消息。例如,其他的消息是退出消息,如果gen_fsm是连接到其他的进程(不是监控者)的,而且需要捕获退出信号。
- handle_info({‘EXIT’, Pid, Reason}, StateName, StateData) ->
- ..code to handle exits here..
- {next_state, StateName1, StateData1}.
cool! 非常好!辛苦了 Godwit 同学!