《Erlang OTP in action》一书完全略过了对gen_fsm的介绍,因为作者认为这是一个很少会用的的behaviour。但是最近看riak_core源代码的时候,发现它的vnode实现是基于gen_fsm的。Erlang/OTP官方文档,
介绍gen_fsm有限状态机的例子代码不全,而且代码的逻辑似乎有问题。当然可能官方认为例子太简单了,我们会自动补全。不过如果每过一段时间重看代码总得再补全总是件麻烦事,在此记下备忘。
简单的说官方文档中提供的是一个开密码锁(code_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
上代码:
简单的说官方文档中提供的是一个开密码锁(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
上代码:
如果觉得这个例子太简单,可以试试fsm做交易的例子。用了两个状态机代表甲乙两方做交易。
http learnyousomeerlang.com/finite-state-machines