《Erlang OTP in action》一书完全略过了对gen_fsm的介绍,因为作者认为这是一个很少会用的的behaviour。但是最近看riak_core源代码的时候,发现它的vnode实现是基于gen_fsm的。Erlang/OTP官方文档(在
这里)介绍gen_fsm有限状态机的例子代码不全,而且代码的逻辑似乎有问题。当然可能官方认为例子太简单了,我们会自动补全。不过如果每过一段时间重看代码总得再补全总是件麻烦事,在此记下备忘。
简单的说官方文档中提供的是一个开密码锁(code_lock)的有限状态机例子。
- 用户在初始启动有限状态机时会设置锁的出厂密码,然后进入“锁定(locked)”状态等待用户按键输入密码;
- 用户通过调用code_lock:button/1输入密码,在用户输入的过程中会记录当前为止录入的健值。如果密码错误或者录入不完整,保持锁定状态。
- 如果密码正确,那么就进入“解锁(open)”状态,并执行相关操作(do_unlock),比如打开大门。
- 当解锁状态持续一段时间后,自动进入锁定状态,并执行相关操作(do_lock),如关门。
密码数据和用户按键输入的数据都用字符串表示。
gen_fsm的状态是由函数表示的,我开始时感觉蛮诡异的,把它理解成当前FSM进程执行到这个状态函数就好了。
从一个状态跳到下一个状态是通过状态函数的返回值控制的,返回值统一这样:
{next_state,NextStateName,NewStateData}
{next_state,NextStateName,NewStateData,Timeout}
{next_state,NextStateName,NewStateData,hibernate}
{stop,Reason,NewStateData}
NextStateName就是下一个状态函数的名字了。
文档中有两个地方提到timeout,一个是gen_fsm:start_link时最后一个控制选项中的timeout,这是控制init/1执行的超时的,不是为FSM进程运行时的状态设置超时。start_link执行时会调用init/1回调函数,直到后者执行完成FSM的启动才算完成,这里的timout控制init/1回调函数的执行不要太久。
似乎不能给FSM进程设置一个缺省的超时,我们必须在每次状态切换(状态函数的返回值,{next_state, _, _, Timeout})时为下一个状态设置超时时间。第一次进入初始状态时的超时设置是在init/1的返回值中设置。(实际上gen_server的超时设置也是这样的)
为了说明超时的使用,我在例子中加入了密码输入时间的控制,如果5秒钟内用户没有输入下一个键,自动清空历史记录,用户必须重新输入。另外还设置了开门时间超过10秒钟,自动关门进入锁定状态。
注意如果FSM在当前状态收到的事件是无法处理的,则整个状态机进程会被迫退出。试试
gen_fsm:send_event(code_lock, foooo).
关于handle_event回调函数:用来处理gen_fsm:send_all_state_event 发送给FSM的事件。无论FSM进程当前处于何种状态,当gen_fsm:send_all_state_event被调用时,状态机会调用handle_event回调函数处理。
关于handle_info回调函数:与gen_server类似,处理所有直接发给FSM进程的消息。例子:
13> code_lock:start_link("abc123").
init: "abc123"
{ok,<0.52.0>}
14> pid(0,52,0) ! hello.
handle_info...
hello
<0.52.0> RECEIVED UNKNOWN EVENT: hello, while FSM process in state: locked
例子的演示,
~/workspace/fsm_test$ erl
Erlang R14B04 (erts-5.8.5) [source] [64-bit] [smp:4:4] [rq:4] [async-threads:0] [hipe] [kernel-poll:false]
Eshell V5.8.5 (abort with ^G)
1> c(code_lock).
{ok,code_lock}
锁的密码设置为abc123
2> code_lock:start_link("abc123").
init: "abc123"
{ok,<0.39.0>}
用户输入密码
3> code_lock:button("ab").
buttion: "ab", So far: [], Code: "abc123"
ok
4> code_lock:button("c").
buttion: "c", So far: "ab", Code: "abc123"
ok
输入完成,密码正确,开门
5> code_lock:button("123").
buttion: "123", So far: "abc", Code: "abc123"
ok
open the DOOR.
发送fooo事件给FSM处理。
6> gen_fsm:send_all_state_event(code_lock, fooo).
handle_event...
ok
<0.39.0> RECEIVED UNKNOWN EVENT: fooo, while FSM process in state: open
7> gen_fsm:send_all_state_event(code_lock, fooo).
handle_event...
ok
<0.39.0> RECEIVED UNKNOWN EVENT: fooo, while FSM process in state: open
8> gen_fsm:send_event(code_lock, foooo).
=ERROR REPORT==== 12-Mar-2012::19:06:46 ===
** State machine code_lock terminating
9> erlang:is_process_alive(pid(0,39,0)).
false
上代码:
-module(code_lock).
-behaviour(gen_fsm).
-export([start_link/1]).
-export([button/1]).
-export([init/1, locked/2, open/2]).
-export([code_change/4, handle_event/3, handle_info/3, handle_sync_event/4, terminate/3]).
-spec(start_link(Code::string()) -> {ok,pid()} | ignore | {error,term()}).
start_link(Code) ->
gen_fsm:start_link({local, code_lock}, code_lock, Code, []).
-spec(button(Digit::string()) -> ok).
button(Digit) ->
gen_fsm:send_event(code_lock, {button, Digit}).
init(LockCode) ->
io:format("init: ~p~n", [LockCode]),
{ok, locked, {[], LockCode}}.
locked({button, Digit}, {SoFar, Code}) ->
io:format("buttion: ~p, So far: ~p, Code: ~p~n", [Digit, SoFar, Code]),
InputDigits = lists:append(SoFar, Digit),
case InputDigits of
Code ->
do_unlock(),
{next_state, open, {[], Code}, 10000};
Incomplete when length(Incomplete)<length(Code) ->
{next_state, locked, {Incomplete, Code}, 5000};
Wrong ->
io:format("wrong passwd: ~p~n", [Wrong]),
{next_state, locked, {[], Code}}
end;
locked(timeout, {_SoFar, Code}) ->
io:format("timout when waiting button inputting, clean the input, button again plz~n"),
{next_state, locked, {[], Code}}.
open(timeout, State) ->
do_lock(),
{next_state, locked, State}.
code_change(_OldVsn, StateName, Data, _Extra) ->
{ok, StateName, Data}.
terminate(normal, _StateName, _Data) ->
ok.
handle_event(Event, StateName, Data) ->
io:format("handle_event... ~n"),
unexpected(Event, StateName),
{next_state, StateName, Data}.
handle_sync_event(Event, From, StateName, Data) ->
io:format("handle_sync_event, for process: ~p... ~n", [From]),
unexpected(Event, StateName),
{next_state, StateName, Data}.
handle_info(Info, StateName, Data) ->
io:format("handle_info...~n"),
unexpected(Info, StateName),
{next_state, StateName, Data}.
%% Unexpected allows to log unexpected messages
unexpected(Msg, State) ->
io:format("~p RECEIVED UNKNOWN EVENT: ~p, while FSM process in state: ~p~n",
[self(), Msg, State]).
%%
%% actions
do_unlock() ->
io:format("passwd is right, open the DOOR.~n").
do_lock() ->
io:format("over, close the DOOR.~n").
如果觉得这个例子太简单,可以试试
这里的用FSM做在线交易的例子。用了两个状态机代表甲乙两方做交易。