前面提到了 otp 中,最常被提到的 gen_server gen_event supervisor,但其實還有另一個 gen_fsm behavior,fsm 就是 finite state machine 有限狀態機的縮寫。
可以在 http://erldocs.com/ 搜尋並取得 gen_fsm 的 API 文件。
FSM finite state machine
finite state machine 有限狀態機,是由一組狀態、一個起始狀態、 輸入、將輸入與現在狀態轉換為下一個狀態的轉換函數所組成。
有許多東西可以用有限狀態來表達,例如紅綠燈、自動販賣機等等,以紅綠燈為例,首先,我們要知道只會有紅、黃、綠三種燈號狀態,這就是有限狀態的條件,絕對不會出現第四種狀態。
在不同狀態下,再搭配不同的輸入條件,就會讓機器從狀態1變化到狀態2。以紅綠燈來說,就是紅燈出現時,在20秒後,就自動變化到綠燈。但狀態變化也是有限制的,因為綠燈 30秒後,會變成黃燈,然後再經過 5秒變紅燈,絕對不會由綠燈直接跳到紅燈,變成黃燈,就只會再變紅燈,絕對不會突然變成綠燈。
state diagram, state transition table
要描述一個 FSM,可以使用 state diagram 或是 state transition table。State Transition Table wiki 裡面,有提到 state transition table 有一維或二維的表示方式,二維表示法還可以選擇將狀態跟改變條件寫在列或table cell 的位置。
以分析的直覺性來看,圖形的方式比較適合人眼觀看與分析,而table 的方式,看起來就比較接近撰寫程式碼的演算法。
目前\下一個 狀態 | Red | Green | Yellow |
---|---|---|---|
Red | X | 20s | X |
Green | X | X | 30s |
Yellow | 5s | X | X |
code_lock 門鎖範例
fsm 官方文件 是以一個門鎖的例子,來說明如何使用 gen_gsm。
門鎖的狀態變化規則如下:
- 初始啟動有限狀態機時會設置鎖的密碼,然後進入 locked 狀態等待用戶按鍵輸入密碼
- 使用者呼叫 code_lock:button/1 輸入密碼,在輸入的過程中會記錄當前為止鍵入的資料。如果密碼錯誤或者不完整,保持 locked 狀態。
- 如果密碼正確,那麼就進入 open 狀態,並執行相關操作do_unlock
- 當 open 狀態持續一段時間後,自動執行相關操作 do_lock,1並進入 locked 狀態
啟動 fsm
gen_fsm 是由呼叫 gen_fsm:start_link({local, code_lock}, code_lock, Code, []) 開始,Code 是初始的門鎖密碼,他會 spawns and links to a new process。
handle_event 與 handle_sync_event 的差異
handle_event 是這兩個 非同步 發送 event 的事件處理函式:
send_event(FsmRef, Event) -> ok
send_all_state_event(FsmRef, Event) -> ok
handle_sync_event 是這兩個 同步 發送 event 的事件處理函式:
sync_send_event(FsmRef, Event, Timeout) -> Reply
sync_send_all_state_event(FsmRef, Event, Timeout) -> Reply
send_event 跟 send_all_state_event 的差異
send_all_state_event 就是所有狀態下該事件的處理方式都是一樣的,而send_event 則是在當前狀態下處理該事件。
拿銀行ATM機來舉例:取消操作是在任何狀態下都可以進程的,而且處理是一樣的,都是回到初始登錄界面,而提款操作則必須要在登錄驗證成功狀態下才能進行,在該狀態下的處理和其他狀態下的處理是不同的。
如果FSM在當前狀態收到的事件是無法處理的,則整個狀態機進程會被迫退出,無論FSM進程當前處於何種狀態,當gen_fsm:send_all_state_event被調用時,fsm 都會呼叫 handle_event callback函數處理。
換句話說,用 send_event 發送事件時,如果 handle_event 在目前 fsm 狀態下無法處理該事件時,此 fsm process會直接結束。
如果用 send_all_state_event,如果 handle_event 在目前 fsm 狀態下無法處理該事件時,不會造成 fsm process 直接結束
狀態中,處理事件的函式
gen_fsm的狀態是用函數表示的
Module:StateName(Event, From, StateData) -> Result
gen_fsm 的狀態處理函數名稱是此 fsm 的狀態,第一個參數 Event 是 send_event 發送的 event 內容,後面的 StateData 是 State 變數資料。
從一個狀態跳到下一個狀態是通過狀態函數的返回值控制的,NextStateName就是下一個狀態函數的名字,返回值統一這樣:
{next_state,NextStateName,NewStateData}
{next_state,NextStateName,NewStateData,Timeout}
{next_state,NextStateName,NewStateData,hibernate}
{stop,Reason,NewStateData}
程式碼
-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]).
%% client functions
% gen_fsm 是由呼叫 code_lock:start_link(Code) 開始,Code 是初始的門鎖密碼
% 他會 spawns and links to a new process
-spec(start_link(Code::string()) -> {ok,pid()} | ignore | {error,term()}).
start_link(Code) ->
% {local, code_lock} 代表註冊在 local 機器,名稱為 code_lock
% 如果寫成 {global, code_lock} 就代表是呼叫了 global:register_name/2
% 第二個參數是所有 call back function 的 module name: code_lock
% 第三個參數是 a list of digits,會傳送到 init
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}).
%% gen_fsm的狀態是由函數表示的
%% Module:StateName(Event, From, StateData) -> Result
%% gen_fsm 的狀態處理函數,前面的 locked, open 是此 fsm 的兩個狀態
%% 第一個參數 {button, Digit} 是 send_event 發送的 event 內容
%% 後面的 {SoFar, Code} 是 State 變數資料
%%
%% 從一個狀態跳到下一個狀態是通過狀態函數的返回值控制的,返回值統一這樣:
%% {next_state,NextStateName,NewStateData}
%% {next_state,NextStateName,NewStateData,Timeout}
%% {next_state,NextStateName,NewStateData,hibernate}
%% {stop,Reason,NewStateData}
%% NextStateName就是下一個狀態函數的名字
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 中記錄的輸入密碼資料清空
locked(timeout, {_SoFar, Code}) ->
io:format("timout when waiting button inputting, clean the input, button again plz~n"),
{next_state, locked, {[], Code}}.
%% 這個是在 open 狀態,timeout 時,會自動呼叫此事件處理函式
open(timeout, State) ->
do_lock(),
{next_state, locked, State}.
%%%
%% 啟動時會被呼叫的函數,回傳值裡面的 State 會在其他函數中使用
init(LockCode) ->
io:format("init: ~p~n", [LockCode]),
%% 初始化 fsm 時,將 fsm 狀態設定為 locked,State 內容設定為 {[], LockCode}
{ok, locked, {[], LockCode}}.
%% 這是 send_event(FsmRef, Event) -> ok, send_all_state_event(FsmRef, Event) -> ok
%% 這兩個 非同步 發送 event 的事件處理函式
%% 當呼叫 gen_fsm:send_event 時,不需要等待 server 回應,就可繼續處理別的事情
%%
%% send_event 跟 send_all_state_event 的差異是
%% 如果FSM在當前狀態收到的事件是無法處理的,則整個狀態機進程會被迫退出
%% 無論FSM進程當前處於何種狀態,當gen_fsm:send_all_state_event被調用時,fsm 都會呼叫 handle_event callback函數處理。
%% 換句話說,用 send_event 發送事件時,如果 handle_event 在目前 fsm 狀態下無法處理該事件時,此 fsm process會直接結束
%% 如果用 send_all_state_event,如果 handle_event 在目前 fsm 狀態下無法處理該事件時,不會造成 fsm process 直接結束
handle_event(Event, StateName, Data) ->
io:format("handle_event... ~n"),
unexpected(Event, StateName),
{next_state, StateName, Data}.
%% 這是 sync_send_event(FsmRef, Event, Timeout) -> Reply, sync_send_all_state_event(FsmRef, Event, Timeout) -> Reply
%% 這兩個 同步 發送 event 的事件處理函式
%% 因為同步的關係,所以發送 event 可設定 Timeout,以避免 fsm 太久沒有回應
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:與gen_server類似,處理所有直接發給 FSM process 的訊息
handle_info(Info, StateName, Data) ->
io:format("handle_info...~n"),
unexpected(Info, StateName),
{next_state, StateName, Data}.
terminate(normal, _StateName, _Data) ->
io:format("terminate...~n"),
ok.
code_change(_OldVsn, StateName, Data, _Extra) ->
{ok, 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").
測試
1> code_lock:start_link("test123").
init: "test123"
{ok,<0.33.0>}
%% 直接發送訊息,是由 handle_info 處理
2> pid(0,33,0)! hello.
handle_info...
<0.33.0> RECEIVED UNKNOWN EVENT: hello, while FSM process in state: locked
hello
3> code_lock:button("ab").
buttion: "ab", So far: [], Code: "test123"
ok
%% 輸入密碼逾時,SoFar 會被清空
4> timout when waiting button inputting, clean the input, button again plz
4> code_lock:button("test").
buttion: "test", So far: [], Code: "test123"
ok
%% 密碼吻合,開門了
5> code_lock:button("123").
buttion: "123", So far: "test", Code: "test123"
passwd is right, open the DOOR.
ok
%% 自動關門
6> close the DOOR.
%% 以 send_all_state_event 發送無法處理的事件 fooo
6> gen_fsm:send_all_state_event(code_lock, fooo).
handle_event...
<0.33.0> RECEIVED UNKNOWN EVENT: fooo, while FSM process in state: locked
ok
%% 以 send_event 發送無法處理的事件 fooo,會造成 fsm process 當機
7> gen_fsm:send_event(code_lock, fooo).
=ERROR REPORT==== 5-Mar-2014::16:00:49 ===
** State machine code_lock terminating
** Last event in was fooo
** When State == locked
** Data == {[],"test123"}
** Reason for termination =
** {function_clause,
[{code_lock,terminate,
[{function_clause,
[{code_lock,locked,
[fooo,{[],"test123"}],
[{file,
"d:/projectcase/erlang/erlangotp/src/code_lock.erl"},
{line,37}]},
{gen_fsm,handle_msg,7,[{file,"gen_fsm.erl"},{line,505}]},
{proc_lib,init_p_do_apply,3,
[{file,"proc_lib.erl"},{line,239}]}]},
locked,
{[],"test123"}],
[{file,"d:/projectcase/erlang/erlangotp/src/code_lock.erl"},
{line,96}]},
{gen_fsm,terminate,7,[{file,"gen_fsm.erl"},{line,597}]},
{proc_lib,init_p_do_apply,3,[{file,"proc_lib.erl"},{line,239}]}]}
** exception exit: function_clause
in function code_lock:terminate/3
called as code_lock:terminate({function_clause,
[{code_lock,locked,
[fooo,{[],"test123"}],
[{file,
"d:/projectcase/erlang/erlangotp/src/
code_lock.erl"},
{line,37}]},
{gen_fsm,handle_msg,7,
[{file,"gen_fsm.erl"},{line,505}]},
{proc_lib,init_p_do_apply,3,
[{file,"proc_lib.erl"},{line,239}]}]},
locked,
{[],"test123"})
in call from gen_fsm:terminate/7 (gen_fsm.erl, line 597)
in call from proc_lib:init_p_do_apply/3 (proc_lib.erl, line 239)
%% fsm process 確實已經被終止了
8> erlang:is_process_alive(pid(0,33,0)).
false
9>
結語
一般在討論 otp 時,只會談到 gen_server、gen_event 與 supervisor,常常會漏掉 gen_fsm,fsm 在可以根據變化的條件來決定並限制某個物件的狀態,這可以幫助我們在預期的情況下,得到物件的狀態變化,而不會因為缺少了狀態變化的判斷,直接設定物件的狀態值,導致該物件的狀態進入了意料之外的情況。
沒有留言:
張貼留言