2014/5/12

erlang otp gen_fsm

前面提到了 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。

門鎖的狀態變化規則如下:

  1. 初始啟動有限狀態機時會設置鎖的密碼,然後進入 locked 狀態等待用戶按鍵輸入密碼
  2. 使用者呼叫 code_lock:button/1 輸入密碼,在輸入的過程中會記錄當前為止鍵入的資料。如果密碼錯誤或者不完整,保持 locked 狀態。
  3. 如果密碼正確,那麼就進入 open 狀態,並執行相關操作do_unlock
  4. 當 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 在可以根據變化的條件來決定並限制某個物件的狀態,這可以幫助我們在預期的情況下,得到物件的狀態變化,而不會因為缺少了狀態變化的判斷,直接設定物件的狀態值,導致該物件的狀態進入了意料之外的情況。