檢視原始碼 gen_statem
行為
建議同時閱讀 STDLIB 中的 gen_statem
參考手冊。
事件驅動狀態機
既有的自動機理論較少探討狀態轉換是如何被觸發的,而是假設輸出是輸入(以及狀態)的函數,並且它們是一些數值。
對於事件驅動狀態機,輸入是一個觸發狀態轉換的事件,而輸出是在狀態轉換期間執行的動作。類似於有限狀態機的數學模型,它可以被描述為以下形式的一組關係:
State(S) x Event(E) -> Actions(A), State(S')
這些關係的解釋如下:如果我們處於狀態 S
,並且發生事件 E
,我們將執行動作 A
,並轉換到狀態 S'
。請注意,S'
可以等於 S
,並且 A
可以是空的。
在 gen_statem
中,我們將狀態改變定義為新狀態 S'
與目前狀態 S
不同的狀態轉換,其中「不同」是指 Erlang 的嚴格不等式:=/=
,也稱為「不匹配」。gen_statem
在狀態改變期間執行的操作比在其他狀態轉換期間多。
由於 A
和 S'
僅取決於 S
和 E
,此處描述的狀態機種類是米利狀態機(請參閱維基百科文章 米利狀態機)。
與大多數 gen_
行為類似,gen_statem
除了狀態之外,還保留一個伺服器 Data
項目。由於這個資料項目,並且由於對狀態數量(假設有足夠的虛擬機器記憶體)或不同輸入事件的數量沒有限制,因此使用此行為實作的狀態機是圖靈完備的。但它感覺更像是一個事件驅動的米利狀態機。
日常狀態機
一個可以建模為狀態機的日常設備示例是經典的原子筆,即可伸縮式原子筆,按下末端即可伸出筆尖,按下側面即可縮回筆尖。(按壓式原子筆也是一個例子,但該類型只有一個事件,因此不太有趣)
---
title: Ballpoint Pen State Diagram
---
stateDiagram-v2
[*] --> Retracted
Retracted --> Retracted : push-side
Retracted --> Exposed : push-end\n* Expose tip
Exposed --> Retracted : push-side\n* Retract tip
Exposed --> Exposed : push-end
狀態圖顯示了狀態、事件和具有轉換動作的狀態轉換。請注意,當筆尖伸出時按下末端,或當筆尖縮回時按下側面,不會改變狀態,也不會引起任何動作,這以返回相同狀態的箭頭來表示。
何時使用 gen_statem
如果您的程序邏輯很適合描述為狀態機,並且您需要以下任何 gen_statem
的關鍵功能,您應該考慮使用 gen_statem
而不是 gen_server
:
- 針對每個狀態、所有事件類型(例如 call、cast 和 info)的共置回呼程式碼
- 延後事件 - 選擇性接收的替代方案
- 插入事件 - 來自狀態機本身的事件;特別是對於純內部事件
- 狀態進入呼叫 - 在狀態條目上回呼,與每個狀態的其餘回呼程式碼共置
- 易於使用的逾時 - 狀態逾時、事件逾時 和 通用逾時(具名逾時)
對於不需要這些功能的簡單狀態機,gen_server
完全適用。它也具有較小的呼叫額外負荷,但我們這裡談論的是大約 2 微秒與 3.3 微秒的呼叫往返時間,因此如果伺服器回呼所做的動作稍微多於僅回覆,或者如果呼叫不那麼頻繁,則很難注意到這種差異。
回呼模組
回呼模組包含實作狀態機的函數。當事件發生時,gen_statem
行為引擎會使用事件、目前狀態和伺服器資料呼叫回呼模組中的函數。此回呼函數會執行事件的動作,並傳回新狀態和伺服器資料,以及由行為引擎執行的動作。
行為引擎會保留狀態機狀態、伺服器資料、計時器參考、已延後訊息的佇列和其他中繼資料。它接收所有程序訊息、處理系統訊息,並使用特定於狀態機的事件呼叫回呼模組。
可以使用任何轉換動作 {change_callback_module, NewModule}
、{push_callback_module, NewModule}
或 pop_callback_module
來變更正在執行的伺服器的回呼模組。
注意
切換回呼模組是一件非常深奧的事情…
此功能的起源是一種在版本協商後,根據通訊協定版本分支到完全不同的狀態機的通訊協定。可能還有其他用例。注意,新的回呼模組會完全取代先前的回呼模組,因此所有相關的回呼函數都必須處理來自先前回呼模組的狀態和資料。
回呼模式
gen_statem
行為支援兩種回呼模式:
state_functions
- 事件由每個狀態一個回呼函數處理。handle_event_function
- 事件由單一回呼函數處理。
回呼模式是回呼模組的屬性,並在伺服器啟動時設定。它可能會因程式碼升級/降級,或在變更回呼模組時而變更。
請參閱狀態回呼章節,其中說明事件處理回呼函數。
回呼模式是透過實作強制性回呼函數 Module:callback_mode()
來選擇的,該函數會傳回其中一個回呼模式。
Module:callback_mode()
函數也可能傳回包含回呼模式和原子 state_enter
的清單,在這種情況下,會為該回呼模式啟用狀態進入呼叫。
選擇回呼模式
簡短版本:選擇 state_functions
- 它最像 gen_fsm
。但是,如果您不希望狀態必須是原子的限制,或者如果您不想為每個狀態寫一個狀態回呼函數,請繼續閱讀…
這兩種回呼模式提供了不同的可能性和限制,並具有一個共同的目標:處理事件和狀態的所有可能組合。
例如,可以透過一次專注於一個狀態,並確保每個狀態都處理所有事件來完成此操作。或者,您可以一次專注於一個事件,並確保在每個狀態中都處理該事件。您也可以混合使用這些策略。
使用 state_functions
時,您會被限制為僅使用原子狀態,並且 gen_statem
引擎會根據狀態名稱進行分支。這會鼓勵回呼模組將特定於一個狀態的所有事件動作的實作共置在程式碼中的同一個位置,因此一次專注於一個狀態。
當您有一個規則的狀態圖時,此模式非常適合,例如本章中的狀態圖,它以視覺方式描述了屬於該狀態的所有事件和動作,並且每個狀態都有其唯一的名稱。
使用 handle_event_function
時,您可以自由地混合策略,因為所有事件和狀態都在同一個回呼函數中處理。
當您想要一次專注於一個事件或一次專注於一個狀態時,此模式同樣有效,但是函數 Module:handle_event/4
會很快變得太大而無法處理,而需要分支到輔助函數。
此模式允許使用非原子狀態,例如,複合狀態,甚至是階層式狀態。請參閱複合狀態章節。舉例來說,如果一個協定的用戶端和伺服器端狀態圖非常相似,你可以擁有一個狀態{StateName, server}
,或 {StateName, client}
,並讓 StateName
決定在程式碼中的哪個位置處理該狀態下的大部分事件。然後,元組的第二個元素會被用來選擇是否處理特定的用戶端或伺服器端事件。
狀態回呼
狀態回呼是在目前狀態中處理事件的回呼函式,而該函式是哪個則取決於回呼模式。
state_functions
- 事件由以下方式處理:Module:StateName(EventType, EventContent, Data)
此形式是範例章節中最常使用的形式。
handle_event_function
- 事件由以下方式處理:Module:handle_event(EventType, EventContent, State, Data)
請參閱單一狀態回呼章節以取得範例。
狀態可以是狀態回呼本身的名稱,或是 handle_event()
回呼的參數。其他參數是 EventType
和事件相關的 EventContent
,兩者都在事件類型和事件內容章節中描述,最後一個參數是目前的伺服器 Data
。
狀態進入呼叫(請參閱該章節)也由事件處理器處理,並且具有稍微不同的參數。
狀態回呼的傳回值在 gen_statem
中 Module:StateName/3
的描述中定義。以下是一個可能更易讀的列表:
{next_state, NextState, NewData [, Actions]}
設定下一個狀態並更新伺服器資料。如果使用Actions
欄位,則執行轉換動作(請參閱該章節)。空的Actions
列表等同於不傳回該欄位。如果
NextState =/= State
,則表示狀態變更,而gen_statem
會執行一些額外的事情:事件佇列會從最舊的延後事件重新開始,任何目前的狀態逾時都會被取消,並且如果啟用了狀態進入呼叫,則會執行該呼叫。目前的State
在狀態進入呼叫中會變成OldState
。{keep_state, NewData [, Actions]}
與next_state
值相同,其中NextState =:= State
,也就是沒有狀態變更。keep_state_and_data | {keep_state_and_data, Actions}
與keep_state
值相同,其中NextData =:= Data
,也就是伺服器資料沒有變更。{repeat_state, NewData [, Actions]} | repeat_state_and_data |{repeat_state_and_data, Actions}
與keep_state
或keep_state_and_data
值相同,但是如果啟用了狀態進入呼叫,則會像重新進入此狀態一樣重複執行。在這種情況下,State
和OldState
在重複的狀態進入呼叫中會變成相等,因為該狀態是從自身重新進入的。{stop, Reason [, NewData]}
以理由Reason
停止伺服器。如果使用NewData
欄位,則先更新伺服器資料。{stop_and_reply, Reason, [NewData, ] ReplyActions}
與stop
值相同,但先執行給定的轉換動作,這些動作只能是回覆動作。
第一個狀態
為了決定第一個狀態,會在呼叫任何狀態回呼之前呼叫 Module:init(Args)
回呼函式。此函式的行為類似於狀態回呼函式,但其唯一的參數 Args
是從 gen_statem
的 start/3,4
或 start_link/3,4
函式取得,並傳回 {ok, State, Data}
或 {ok, State, Data, Actions}
。如果你從此函式使用 postpone
動作,該動作會被忽略,因為沒有事件可以延後。
轉換動作
在第一節(事件驅動狀態機)中,動作被提及為一般狀態機模型的一部分。這些一般動作是使用回呼模組 gen_statem
在事件處理回呼函式中執行,然後再返回 gen_statem
引擎的程式碼來實作。
還有更特定的轉換動作,回呼函式可以命令 gen_statem
引擎在回呼函式返回後執行。這些動作是透過從 回呼函式的 傳回值中傳回 動作列表來命令的。以下是可能的轉換動作:
{postpone, Boolean}
- 如果true
,則延後目前事件,請參閱延後事件章節。{hibernate, Boolean
- 如果true
,則休眠gen_statem
,在休眠章節中處理。{state_timeout, Time, EventContent [, Opts]}
|
{state_timeout, update, EventContent}
|
{state_timeout, cancel}
- 啟動、更新或取消狀態逾時,請在逾時和狀態逾時章節中閱讀更多內容。{{timeout, Name}, Time, EventContent [, Opts]}
|
{{timeout, Name}, update, EventContent}
|
{{timeout, Name}, cancel}
- 啟動、更新或取消通用逾時,請在逾時和通用逾時章節中閱讀更多內容。{timeout, Time, EventContent [, Opts]}
- 啟動事件逾時,請在逾時和事件逾時章節中閱讀更多內容。{reply, From, Reply}
- 回覆呼叫者,在所有狀態事件章節的末尾提及。{next_event, EventType, EventContent}
- 產生下一個要處理的事件,請參閱插入事件章節。{change_callback_module, NewModule}
- 變更正在執行的伺服器的回呼模組。這可以在任何狀態轉換期間完成,無論是狀態變更與否,但不能從狀態進入呼叫中完成。{push_callback_module, NewModule}
- 將目前的回呼模組推送到回呼模組的內部堆疊頂部,並為正在執行的伺服器設定新的回呼模組。否則與上面的{change_callback_module, NewModule}
相同。pop_callback_module
- 從回呼模組的內部堆疊中彈出頂部模組,並將其設定為正在執行的伺服器的新回呼模組。如果堆疊為空,則伺服器會失敗。否則與上面的{change_callback_module, NewModule}
相同。
有關詳細資訊,請參閱模組 gen_statem
的 action()
類型。例如,你可以回覆多個呼叫者,產生多個下一個事件,並設定逾時以使用絕對時間而不是相對時間(使用 Opts
欄位)。
在這些轉換動作中,唯一立即的動作是 reply
來回覆呼叫者。其他動作會在狀態轉換期間稍後收集和處理。插入事件會全部儲存並插入,其餘的則設定轉換選項,其中特定類型的最後一個會覆寫前一個。請參閱模組 gen_statem
中 transition_option()
類型的狀態轉換描述。
不同的逾時和 next_event
動作會產生具有相應事件類型和事件內容的新事件。
事件類型和事件內容
事件會分類為不同的事件類型。所有類型的事件都會在給定狀態下於同一個回呼函式中處理,而該函式會取得 EventType
和 EventContent
作為參數。EventContent
的含義取決於 EventType
。
以下是事件類型的完整列表以及它們的來源:
cast
- 由gen_statem:cast(ServerRef, Msg)
產生,其中Msg
變成EventContent
。{call, From}
- 由gen_statem:call(ServerRef, Request)
、gen_statem:send_request(ServerRef, Request)
或gen_statem:send_request(ServerRef, Request, _, _)
產生,其中Request
變成EventContent
。From
是回覆地址,用於透過轉換動作{reply, From, Reply}
回覆,或從回呼模組呼叫gen_statem:reply(From, Reply)
時使用。info
- 由傳送到gen_statem
程序的任何常規程序訊息產生。程序訊息會變成EventContent
。state_timeout
- 由轉換動作{state_timeout, Time, EventContent}
在逾時到期時產生。請在逾時和狀態逾時章節中閱讀更多內容。{timeout, Name}
- 由轉換動作{{timeout, Name},Time, EventContent}
在逾時到期時產生。請在逾時和通用逾時章節中閱讀更多內容。timeout
- 由轉換動作{timeout, Time, EventContent}
(或其簡寫形式Time
) 在逾時時間到期時產生。請參閱 逾時 和 事件逾時 章節了解更多資訊。internal
- 由轉換動作{next_event, internal, EventContent}
產生。上述所有事件類型也可以使用next_event
動作產生:{next_event, EventType, EventContent}
。
狀態進入呼叫
如果啟用此功能,則 gen_statem
行為無論回呼模式為何,都會在狀態變更時自動呼叫 狀態回呼 並帶有特殊引數,因此您可以在其餘的狀態轉換規則附近編寫狀態進入動作。它通常看起來像這樣
StateName(enter, OldState, Data) ->
... code for state enter actions here ...
{keep_state, NewData};
StateName(EventType, EventContent, Data) ->
... code for actions here ...
{next_state, NewStateName, NewData}.
由於狀態進入呼叫不是一個事件,因此對於允許的回傳值和狀態 轉換動作 有限制。您不得變更狀態、延後此非事件、插入任何事件,或變更回呼模組。
在 gen_statem:init/1
之後進入的第一個狀態將會收到一個狀態進入呼叫,其中 OldState
等於目前狀態。
您可以使用來自 狀態回呼 的 {repeat_state,...}
回傳值來重複狀態進入呼叫。在這種情況下,OldState
也會等於目前狀態。
根據您的狀態機器的指定方式,這可能是一個非常有用的功能,但它會強制您在所有狀態中處理狀態進入呼叫。另請參閱 狀態進入動作 章節。
逾時
gen_statem
中的逾時是從狀態轉換期間的 轉換動作 開始,也就是從 狀態回呼 退出時開始。
gen_statem
中有 3 種逾時類型
state_timeout
- 有一個 狀態逾時,它會因狀態變更而自動取消。{timeout, Name}
- 有任意數量的 通用逾時,它們的Name
各不相同。它們沒有自動取消功能。
當逾時開始時,任何相同類型的正在執行的逾時(state_timeout
、{timeout, Name}
或 timeout
)都會被取消,也就是說,逾時會以新的時間和事件內容重新開始。
所有逾時都有一個 EventContent
,它是啟動逾時的 轉換動作 的一部分。不同的 EventContent
不會建立不同的逾時。EventContent
會在逾時到期時傳遞到 狀態回呼。
取消逾時
以 infinity
時間值啟動逾時永遠不會逾時,這是透過甚至不啟動它來最佳化,並且任何具有相同標籤的正在執行的逾時都將被取消。在這種情況下,EventContent
將被忽略,因此將其設定為 undefined
是合理的。
取消逾時更明確的方式是使用 轉換動作,其形式為 {TimeoutType, cancel}
。
更新逾時
當逾時正在執行時,可以使用 轉換動作,其形式為 {TimeoutType, update, NewEventContent}
,來更新其 EventContent
。
如果在沒有此類 TimeoutType
正在執行時使用此功能,則會立即傳遞逾時事件,就像啟動零逾時時一樣。
零逾時
如果逾時以時間 0
啟動,它實際上不會啟動。相反地,逾時事件會立即插入,以便在任何已排隊的事件之後且在任何尚未收到的外部事件之前進行處理。
請注意,某些逾時會自動取消,因此,如果您在狀態變更中將延後事件與啟動時間為 0
的事件逾時結合,則不會插入任何逾時事件,因為事件逾時會因延後的事件而取消,而延後的事件是由於狀態變更而傳遞的。
範例
帶有密碼鎖的門可以看作是一個狀態機器。最初,門是鎖著的。當有人按下按鈕時,會產生一個 {button, Button}
事件。在下方的狀態圖中,「收集按鈕」表示將按鈕儲存到與正確密碼中的按鈕一樣多;附加到長度限制的清單。如果正確,則門會解鎖 10 秒。如果不正確,我們會等待按下新的按鈕。
---
title: Code Lock State Diagram
---
stateDiagram-v2
state check_code <<choice>>
[*] --> locked : * do_lock()\n* Clear Buttons
locked --> check_code : {button, Button}\n* Collect Buttons
check_code --> locked : Incorrect code
check_code --> open : Correct code\n* do_unlock()\n* Clear Buttons\n* Set state_timeout 10 s
open --> open : {button, Digit}
open --> locked : state_timeout\n* do_lock()
此密碼鎖狀態機器可以使用 gen_statem
和以下回呼模組實作
-module(code_lock).
-behaviour(gen_statem).
-define(NAME, code_lock).
-export([start_link/1]).
-export([button/1]).
-export([init/1,callback_mode/0,terminate/3]).
-export([locked/3,open/3]).
start_link(Code) ->
gen_statem:start_link({local,?NAME}, ?MODULE, Code, []).
button(Button) ->
gen_statem:cast(?NAME, {button,Button}).
init(Code) ->
do_lock(),
Data = #{code => Code, length => length(Code), buttons => []},
{ok, locked, Data}.
callback_mode() ->
state_functions.
locked(
cast, {button,Button},
#{code := Code, length := Length, buttons := Buttons} = Data) ->
NewButtons =
if
length(Buttons) < Length ->
Buttons;
true ->
tl(Buttons)
end ++ [Button],
if
NewButtons =:= Code -> % Correct
do_unlock(),
{next_state, open, Data#{buttons := []},
[{state_timeout,10_000,lock}]}; % Time in milliseconds
true -> % Incomplete | Incorrect
{next_state, locked, Data#{buttons := NewButtons}}
end.
open(state_timeout, lock, Data) ->
do_lock(),
{next_state, locked, Data};
open(cast, {button,_}, Data) ->
{next_state, open, Data}.
do_lock() ->
io:format("Lock~n", []).
do_unlock() ->
io:format("Unlock~n", []).
terminate(_Reason, State, _Data) ->
State =/= locked andalso do_lock(),
ok.
程式碼將在下一節中說明。
啟動 gen_statem
在上一節的範例中,gen_statem
是透過呼叫 code_lock:start_link(Code)
啟動的
start_link(Code) ->
gen_statem:start_link({local,?NAME}, ?MODULE, Code, []).
start_link/1
呼叫函數 gen_statem:start_link/4
,它會衍生並連結到一個新的處理序,也就是一個 gen_statem
。
第一個引數
{local,?NAME}
指定名稱。在此情況下,gen_statem
會透過巨集?NAME
在本機註冊為code_lock
。如果省略名稱,則不會註冊
gen_statem
。而是必須使用其 pid。名稱也可以指定為{global, Name}
,然後會使用 Kernel 中的global:register_name/2
來註冊gen_statem
。第二個引數
?MODULE
是回呼模組的名稱,也就是回呼函數所在的模組,也就是這個模組。介面函數(
start_link/1
和button/1
)與回呼函數(init/1
、locked/3
和open/3
)位於同一個模組中。通常良好的程式設計實務是將用戶端程式碼和伺服器端程式碼包含在同一個模組中。第三個引數
Code
是一個數字清單,它是傳遞至回呼函數init/1
的正確解鎖密碼。第四個引數
[]
是一個選項清單。如需可用的選項,請參閱gen_statem:start_link/3
。
如果名稱註冊成功,則新的 gen_statem
處理序會呼叫回呼函數 code_lock:init(Code)
。預期此函數會回傳 {ok, State, Data}
,其中 State
是 gen_statem
的初始狀態,在此情況下為 locked
;假設門一開始是鎖著的。Data
是 gen_statem
的內部伺服器資料。這裡的伺服器資料是一個 map()
,其中鍵 code
儲存正確的按鈕順序,鍵 length
儲存其長度,鍵 buttons
儲存收集到的按鈕直到相同的長度。
init(Code) ->
do_lock(),
Data = #{code => Code, length => length(Code), buttons => []},
{ok, locked, Data}.
函數 gen_statem:start_link/3,4
是同步的。它只有在 gen_statem
初始化並準備好接收事件後才會回傳。
如果 gen_statem
是監督樹的一部分,也就是由監督者啟動,則必須使用函數 gen_statem:start_link/3,4
。函數 gen_statem:start/3,4
可以用來啟動獨立的 gen_statem
,也就是說,它不是監督樹的一部分。
函數 Module:callback_mode/0
會為回呼模組選取 CallbackMode
,在此情況下為 state_functions
。也就是說,每個狀態都有自己的處理函數
callback_mode() ->
state_functions.
處理事件
使用 gen_statem:cast/2
實作通知密碼鎖按鈕事件的函數
button(Button) ->
gen_statem:cast(?NAME, {button,Button}).
第一個引數是 gen_statem
的名稱,且必須與用來啟動它的名稱一致。因此,我們使用與啟動時相同的巨集 ?NAME
。{button, Button}
是事件內容。
事件會傳送到 gen_statem
。當收到事件時,gen_statem
會呼叫 StateName(cast, Event, Data)
,預期它會回傳一個元組 {next_state, NewStateName, NewData}
或 {next_state, NewStateName, NewData, Actions}
。StateName
是目前狀態的名稱,而 NewStateName
是下一個狀態的名稱。NewData
是 gen_statem
的伺服器資料的新值,而 Actions
是 gen_statem
引擎要執行的動作清單。
locked(
cast, {button,Button},
#{code := Code, length := Length, buttons := Buttons} = Data) ->
NewButtons =
if
length(Buttons) < Length ->
Buttons;
true ->
tl(Buttons)
end ++ [Button],
if
NewButtons =:= Code -> % Correct
do_unlock(),
{next_state, open, Data#{buttons := []},
[{state_timeout,10_000,lock}]}; % Time in milliseconds
true -> % Incomplete | Incorrect
{next_state, locked, Data#{buttons := NewButtons}}
end.
在 locked
狀態中,當按下按鈕時,它會與先前按下的按鈕收集到與正確密碼的長度相同,然後與正確密碼進行比較。根據結果,門會被解鎖且 gen_statem
會進入 open
狀態,或者門會保持在 locked
狀態。
當變更為 open
狀態時,收集到的按鈕會重設,鎖會被解鎖,並啟動一個 10 秒的狀態逾時。
open(cast, {button,_}, Data) ->
{next_state, open, Data}.
在 open
狀態中,會忽略按鈕事件,並保持在相同的狀態。這也可以透過回傳 {keep_state, Data}
來完成,或者在這種情況下,由於 Data
未變更,因此可以透過回傳 keep_state_and_data
來完成。
狀態逾時
當給出正確的密碼時,門會被解鎖,並且會從 locked/2
回傳下列元組
{next_state, open, Data#{buttons := []},
[{state_timeout,10_000,lock}]}; % Time in milliseconds
10,000 是以毫秒為單位的逾時值。在此時間(10 秒)之後,會發生逾時。然後,會呼叫 StateName(state_timeout, lock, Data)
。當門在 open
狀態 10 秒後,會發生逾時。之後,門會再次被鎖上
open(state_timeout, lock, Data) ->
do_lock(),
{next_state, locked, Data};
當狀態機器進行狀態變更時,會自動取消狀態逾時的計時器。
您可以重新啟動、取消或更新狀態逾時。詳情請參閱逾時章節。
所有狀態事件
有時事件可能在 gen_statem
的任何狀態中到達。在所有狀態函式為非特定於該狀態的事件呼叫的通用狀態處理函式中處理這些事件是很方便的。
考慮一個 code_length/0
函式,它會傳回正確程式碼的長度。我們將所有非特定於狀態的事件分派到通用函式 handle_common/3
...
-export([button/1,code_length/0]).
...
code_length() ->
gen_statem:call(?NAME, code_length).
...
locked(...) -> ... ;
locked(EventType, EventContent, Data) ->
handle_common(EventType, EventContent, Data).
...
open(...) -> ... ;
open(EventType, EventContent, Data) ->
handle_common(EventType, EventContent, Data).
handle_common({call,From}, code_length, #{code := Code} = Data) ->
{keep_state, Data,
[{reply,From,length(Code)}]}.
另一種方法是透過便利巨集 ?HANDLE_COMMON/0
...
-export([button/1,code_length/0]).
...
code_length() ->
gen_statem:call(?NAME, code_length).
-define(HANDLE_COMMON,
?FUNCTION_NAME(T, C, D) -> handle_common(T, C, D)).
%%
handle_common({call,From}, code_length, #{code := Code} = Data) ->
{keep_state, Data,
[{reply,From,length(Code)}]}.
...
locked(...) -> ... ;
?HANDLE_COMMON.
...
open(...) -> ... ;
?HANDLE_COMMON.
此範例使用 gen_statem:call/2
,它會等待伺服器的回覆。回覆會在保留目前狀態的 {keep_state, ...}
元組中的動作清單中,以 {reply, From, Reply}
元組傳送。當您想要保持在目前狀態,但不知道或不關心它是什麼時,此傳回形式很方便。
如果通用的狀態回呼需要知道目前狀態,則可以使用函式 handle_common/4
。
-define(HANDLE_COMMON,
?FUNCTION_NAME(T, C, D) -> handle_common(T, C, ?FUNCTION_NAME, D)).
單一狀態回呼
如果使用回呼模式 handle_event_function
,所有事件都會在 Module:handle_event/4
中處理,而且我們可以(但不必)使用以事件為中心的方法,先根據事件進行分支,然後再根據狀態進行分支。
...
-export([handle_event/4]).
...
callback_mode() ->
handle_event_function.
handle_event(cast, {button,Button}, State, #{code := Code} = Data) ->
case State of
locked ->
#{length := Length, buttons := Buttons} = Data,
NewButtons =
if
length(Buttons) < Length ->
Buttons;
true ->
tl(Buttons)
end ++ [Button],
if
NewButtons =:= Code -> % Correct
do_unlock(),
{next_state, open, Data#{buttons := []},
[{state_timeout,10_000,lock}]}; % Time in milliseconds
true -> % Incomplete | Incorrect
{keep_state, Data#{buttons := NewButtons}}
end;
open ->
keep_state_and_data
end;
handle_event(state_timeout, lock, open, Data) ->
do_lock(),
{next_state, locked, Data};
handle_event(
{call,From}, code_length, _State, #{code := Code} = Data) ->
{keep_state, Data,
[{reply,From,length(Code)}]}.
...
停止
在監管樹中
如果 gen_statem
是監管樹的一部分,則不需要停止函式。gen_statem
會由其監管者自動終止。確切的執行方式由監管者中設定的關閉策略定義。
如果需要在終止前清除,關閉策略必須是逾時值,而且 gen_statem
必須在函式 init/1
中,透過呼叫 process_flag(trap_exit, true)
將自身設定為捕獲結束訊號。
init(Args) ->
process_flag(trap_exit, true),
do_lock(),
...
當被命令關閉時,gen_statem
接著會呼叫回呼函式 terminate(shutdown, State, Data)
。
在此範例中,函式 terminate/3
會在門開啟時將其鎖上,因此我們不會在監管樹終止時意外地讓門開啟。
terminate(_Reason, State, _Data) ->
State =/= locked andalso do_lock(),
ok.
獨立 gen_statem
如果 gen_statem
不是監管樹的一部分,則可以使用 gen_statem:stop/1
停止它,最好透過 API 函式停止。
...
-export([start_link/1,stop/0]).
...
stop() ->
gen_statem:stop(?NAME).
這會讓 gen_statem
呼叫回呼函式 terminate/3
,就像受監管的伺服器一樣,並等待程序終止。
事件逾時
從 gen_statem
的前身 gen_fsm
繼承的逾時功能是事件逾時,也就是說,如果事件到達,計時器就會取消。您會收到事件或逾時,但不會兩者都收到。
它由轉換動作 {timeout, Time, EventContent}
或僅僅整數 Time
來排序,即使沒有括住的動作清單也一樣(後者是從 gen_fsm
繼承的形式)。
例如,這種逾時類型對於根據非活動狀態採取動作很有用。如果 30 秒內沒有按下任何按鈕,讓我們重新啟動程式碼序列
...
locked(timeout, _, Data) ->
{next_state, locked, Data#{buttons := []}};
locked(
cast, {button,Button},
#{code := Code, length := Length, buttons := Buttons} = Data) ->
...
true -> % Incomplete | Incorrect
{next_state, locked, Data#{buttons := NewButtons},
30_000} % Time in milliseconds
...
每當我們收到按鈕事件時,我們都會啟動 30 秒的事件逾時,如果我們收到timeout
的事件類型,我們會重設剩餘的程式碼序列。
事件逾時會被任何其他事件取消,因此您會收到其他事件或逾時事件。因此,取消、重新啟動或更新事件逾時既不可能也沒必要。您採取行動的任何事件都已取消事件逾時,因此在執行狀態回呼時,永遠不會有正在執行的事件逾時。
請注意,當您有例如 所有狀態事件 章節中的狀態呼叫,或處理未知事件時,事件逾時並不能很好地運作,因為所有類型的事件都會取消事件逾時。
通用逾時
先前的狀態逾時範例僅在狀態機器在逾時期間保持在相同狀態時才有效。而且,只有在沒有發生不相關的干擾事件時,事件逾時才有效。
您可能想要在一個狀態中啟動計時器,並在另一個狀態中回應逾時,也許可以在不變更狀態的情況下取消逾時,或者可能並行執行多個逾時。所有這些都可以使用通用逾時來完成。它們可能看起來有點像 事件逾時,但包含一個名稱,可同時允許任意數量的逾時,而且它們不會自動取消。
以下說明如何透過改用名為例如 open
的通用逾時,來完成先前範例中的狀態逾時。
...
locked(
cast, {button,Button},
#{code := Code, length := Length, buttons := Buttons} = Data) ->
...
if
NewButtons =:= Code -> % Correct
do_unlock(),
{next_state, open, Data#{buttons := []},
[{{timeout,open},10_000,lock}]}; % Time in milliseconds
...
open({timeout,open}, lock, Data) ->
do_lock(),
{next_state,locked,Data};
open(cast, {button,_}, Data) ->
{keep_state,Data};
...
特定的通用逾時可以像狀態逾時一樣,透過將其設定為新的時間或 infinity
來重新啟動或取消。
在此特定情況下,我們不需要取消逾時,因為逾時事件是唯一可能將狀態變更從 open
變更為 locked
的原因。
與其煩惱何時取消逾時,不如在已知為遲到的狀態中忽略延遲的逾時事件來處理它。
您可以重新啟動、取消或更新通用逾時。詳情請參閱逾時章節。
Erlang 計時器
處理逾時最通用的方法是使用 Erlang 計時器;請參閱 erlang:start_timer/3,4
。大多數逾時任務可以使用 gen_statem
中的逾時功能來執行,但一個不能執行的範例是,如果您需要來自 erlang:cancel_timer(Tref)
的傳回值,也就是計時器的剩餘時間。
以下說明如何透過改用 Erlang 計時器來完成先前範例中的狀態逾時
...
locked(
cast, {button,Button},
#{code := Code, length := Length, buttons := Buttons} = Data) ->
...
if
NewButtons =:= Code -> % Correct
do_unlock(),
Tref =
erlang:start_timer(
10_000, self(), lock), % Time in milliseconds
{next_state, open, Data#{buttons := [], timer => Tref}};
...
open(info, {timeout,Tref,lock}, #{timer := Tref} = Data) ->
do_lock(),
{next_state,locked,maps:remove(timer, Data)};
open(cast, {button,_}, Data) ->
{keep_state,Data};
...
當我們將狀態變更為 locked
時,從地圖中移除 timer
鍵並非絕對必要,因為我們只能透過更新的 timer
地圖值進入狀態 open
。但最好不要在狀態 Data
中擁有過時的值。
如果您需要因為其他事件而取消計時器,則可以使用 erlang:cancel_timer(Tref)
。請注意,在此之後不會收到任何逾時訊息(因為計時器已明確取消),除非您先前已延遲一個(請參閱下一節),因此請確保您不會意外延遲此類訊息。另請注意,逾時訊息可能會在取消計時器的狀態回呼期間到達,因此您可能必須從程序信箱中讀取此類訊息,這取決於來自 erlang:cancel_timer(Tref)
的傳回值。
處理延遲逾時的另一種方式可以是不要取消它,而是在已知為遲到的狀態中忽略它。
延遲事件
如果您想要忽略目前狀態中的特定事件,並在未來的狀態中處理它,您可以延遲該事件。延遲的事件會在狀態變更之後重試,也就是說,OldState =/= NewState
。
延遲由轉換動作 postpone
排序。
在此範例中,我們可以延遲按鈕事件,而不是在 open
狀態時忽略它們,稍後在 locked
狀態中處理它們。
...
open(cast, {button,_}, Data) ->
{keep_state,Data,[postpone]};
...
由於延遲的事件僅在狀態變更之後重試,因此您必須考慮將狀態資料項目保留在何處。您可以將其保留在伺服器 Data
中或在 State
本身中,例如透過擁有兩個或多個或少相同的狀態來保留布林值,或透過使用具有 回呼模式 handle_event_function
的複雜狀態(請參閱 複雜狀態 章節)。如果值的變更會變更處理的事件集,則該值應該在 State 中。否則,只會變更伺服器 Data
,因此不會重試任何延遲的事件。
如果事件被延遲,這一點很重要。但請記住,關於狀態中應包含哪些內容的不正確設計決策,可能會在稍後引入事件延遲時,變成難以找到的錯誤。
模糊狀態圖
狀態圖未指定如何處理圖表中特定狀態未說明的事件是很常見的。希望這會在相關的文字或從上下文中描述。
可能的動作:忽略,例如捨棄事件(也許會記錄它)或在某些其他狀態中處理事件,例如延遲它。
選擇性接收
Erlang 的選擇性 receive
陳述式通常用於描述直接 Erlang 程式碼中的簡單狀態機器範例。以下是第一個範例的可能實作
-module(code_lock).
-define(NAME, code_lock_1).
-export([start_link/1,button/1]).
start_link(Code) ->
spawn(
fun () ->
true = register(?NAME, self()),
do_lock(),
locked(Code, length(Code), [])
end).
button(Button) ->
?NAME ! {button,Button}.
locked(Code, Length, Buttons) ->
receive
{button,Button} ->
NewButtons =
if
length(Buttons) < Length ->
Buttons;
true ->
tl(Buttons)
end ++ [Button],
if
NewButtons =:= Code -> % Correct
do_unlock(),
open(Code, Length);
true -> % Incomplete | Incorrect
locked(Code, Length, NewButtons)
end
end.
open(Code, Length) ->
receive
after 10_000 -> % Time in milliseconds
do_lock(),
locked(Code, Length, [])
end.
do_lock() ->
io:format("Locked~n", []).
do_unlock() ->
io:format("Open~n", []).
在這種情況下,選擇性接收會導致 open
將任何事件隱式延遲到 locked
狀態。
絕對不應該在 gen_statem
行為(或任何 gen_*
行為)中使用萬用接收 (catch-all receive),因為接收語句本身就在 gen_*
引擎內。與 sys
相容的行為必須回應系統訊息,因此它們會在引擎的接收迴圈中執行此操作,並將非系統訊息傳遞給回呼模組。使用萬用接收可能會導致系統訊息被丟棄,進而導致無法預期的行為。如果必須使用選擇性接收,則應格外小心,確保只接收與操作相關的訊息。同樣地,回呼必須在適當的時間內返回,以便讓引擎接收迴圈處理系統訊息,否則它們可能會逾時,也可能導致無法預期的行為。
轉換動作 postpone
設計用於模擬選擇性接收。選擇性接收會隱式延遲任何尚未接收到的事件,但 postpone
轉換動作則會顯式延遲單一接收到的事件。
兩種機制在理論上具有相同時間和記憶體複雜度,但請注意,選擇性接收語言結構具有較小的常數因子。
狀態進入動作
假設您有一個狀態機規範,其中使用了狀態進入動作。雖然您可以使用插入的事件(在下一節中描述)來編碼此規範,尤其是當只有一個或幾個狀態具有狀態進入動作時,這正是使用內建 狀態進入呼叫 的完美案例。
您從 callback_mode/0
函數返回包含 state_enter
的列表,當 gen_statem
引擎執行狀態變更時,會使用事件 (enter, OldState, ...)
呼叫您的狀態回呼一次。然後,您只需要在所有狀態中處理這些類似事件的呼叫。
...
init(Code) ->
process_flag(trap_exit, true),
Data = #{code => Code, length = length(Code)},
{ok, locked, Data}.
callback_mode() ->
[state_functions,state_enter].
locked(enter, _OldState, Data) ->
do_lock(),
{keep_state,Data#{buttons => []}};
locked(
cast, {button,Button},
#{code := Code, length := Length, buttons := Buttons} = Data) ->
...
if
NewButtons =:= Code -> % Correct
{next_state, open, Data};
...
open(enter, _OldState, _Data) ->
do_unlock(),
{keep_state_and_data,
[{state_timeout,10_000,lock}]}; % Time in milliseconds
open(state_timeout, lock, Data) ->
{next_state, locked, Data};
...
您可以透過返回 {repeat_state, ...}
、{repeat_state_and_data, _}
或 repeat_state_and_data
之一來重複狀態進入程式碼,這些程式碼的行為方式與 keep_state
同級別的程式碼完全相同。請參閱參考手冊中的 state_callback_result()
類型。
插入的事件
有時能夠向您自己的狀態機產生事件會很有好處。可以使用 轉換動作 {next_event, EventType, EventContent}
來完成此操作。
您可以產生任何現有 type 的事件,但 internal
類型只能透過動作 next_event
產生。因此,它不能來自外部來源,因此您可以確定 internal
事件是從您的狀態機到其自身的事件。
一個例子是預先處理傳入的資料,例如解密區塊或收集字元直到換行符號。
純粹主義者可能會認為這應該使用一個單獨的狀態機來建模,該狀態機會將預先處理的事件發送到主狀態機。
然而,為了效率起見,可以將小的預處理狀態機整合到主狀態機的通用事件處理中。這種整合涉及使用幾個狀態資料項目,將預先處理的事件作為內部事件分派到主狀態機。
使用內部事件也可以更容易地同步狀態機。
另一種變體是使用具有 一個狀態回呼的複雜狀態,例如,使用元組 {MainFSMState, SubFSMState}
來建模狀態。
為了說明這一點,我們舉一個例子,其中按鈕改為產生向下和向上(按下和釋放)事件,並且鎖僅在相應的向下事件之後才回應向上事件。
...
-export([down/1, up/1]).
...
down(Button) ->
gen_statem:cast(?NAME, {down,Button}).
up(Button) ->
gen_statem:cast(?NAME, {up,Button}).
...
locked(enter, _OldState, Data) ->
do_lock(),
{keep_state,Data#{buttons => []}};
locked(
internal, {button,Button},
#{code := Code, length := Length, buttons := Buttons} = Data) ->
...
handle_common(cast, {down,Button}, Data) ->
{keep_state, Data#{button => Button}};
handle_common(cast, {up,Button}, Data) ->
case Data of
#{button := Button} ->
{keep_state,maps:remove(button, Data),
[{next_event,internal,{button,Button}}]};
#{} ->
keep_state_and_data
end;
...
open(internal, {button,_}, Data) ->
{keep_state,Data,[postpone]};
...
如果您使用 code_lock:start([17])
啟動此程式,您可以使用 code_lock:down(17), code_lock:up(17).
解鎖。
範例重新審視
本節包含在大部分提及的修改之後的範例,以及使用狀態進入呼叫的更多範例,這值得一個新的狀態圖。
---
title: Code Lock State Diagram Revisited
---
stateDiagram-v2
state enter_locked <<choice>>
state enter_open <<choice>>
state check_code <<choice>>
[*] --> enter_locked
enter_locked --> locked : * do_lock()\n* Clear Buttons
locked --> check_code : {button, Button}\n* Collect Buttons
locked --> locked : state_timeout\n* Clear Buttons
check_code --> locked : Incorrect code\n* Set state_timeout 30 s
check_code --> enter_open : Correct code
enter_open --> open : * do_unlock()\n* Set state_timeout 10 s
open --> enter_locked : state_timeout
請注意,此狀態圖未指定如何在 open
狀態下處理按鈕事件。因此,您需要讀取一些旁註,也就是這裡:未指定的事件應延遲(在稍後的狀態中處理)。此外,狀態圖未顯示 code_length/0
呼叫必須在每個狀態中處理。
回呼模式:state_functions
使用狀態函數
-module(code_lock).
-behaviour(gen_statem).
-define(NAME, code_lock_2).
-export([start_link/1,stop/0]).
-export([down/1,up/1,code_length/0]).
-export([init/1,callback_mode/0,terminate/3]).
-export([locked/3,open/3]).
start_link(Code) ->
gen_statem:start_link({local,?NAME}, ?MODULE, Code, []).
stop() ->
gen_statem:stop(?NAME).
down(Button) ->
gen_statem:cast(?NAME, {down,Button}).
up(Button) ->
gen_statem:cast(?NAME, {up,Button}).
code_length() ->
gen_statem:call(?NAME, code_length).
init(Code) ->
process_flag(trap_exit, true),
Data = #{code => Code, length => length(Code), buttons => []},
{ok, locked, Data}.
callback_mode() ->
[state_functions,state_enter].
-define(HANDLE_COMMON,
?FUNCTION_NAME(T, C, D) -> handle_common(T, C, D)).
%%
handle_common(cast, {down,Button}, Data) ->
{keep_state, Data#{button => Button}};
handle_common(cast, {up,Button}, Data) ->
case Data of
#{button := Button} ->
{keep_state, maps:remove(button, Data),
[{next_event,internal,{button,Button}}]};
#{} ->
keep_state_and_data
end;
handle_common({call,From}, code_length, #{code := Code}) ->
{keep_state_and_data,
[{reply,From,length(Code)}]}.
locked(enter, _OldState, Data) ->
do_lock(),
{keep_state, Data#{buttons := []}};
locked(state_timeout, button, Data) ->
{keep_state, Data#{buttons := []}};
locked(
internal, {button,Button},
#{code := Code, length := Length, buttons := Buttons} = Data) ->
NewButtons =
if
length(Buttons) < Length ->
Buttons;
true ->
tl(Buttons)
end ++ [Button],
if
NewButtons =:= Code -> % Correct
{next_state, open, Data};
true -> % Incomplete | Incorrect
{keep_state, Data#{buttons := NewButtons},
[{state_timeout,30_000,button}]} % Time in milliseconds
end;
?HANDLE_COMMON.
open(enter, _OldState, _Data) ->
do_unlock(),
{keep_state_and_data,
[{state_timeout,10_000,lock}]}; % Time in milliseconds
open(state_timeout, lock, Data) ->
{next_state, locked, Data};
open(internal, {button,_}, _) ->
{keep_state_and_data, [postpone]};
?HANDLE_COMMON.
do_lock() ->
io:format("Locked~n", []).
do_unlock() ->
io:format("Open~n", []).
terminate(_Reason, State, _Data) ->
State =/= locked andalso do_lock(),
ok.
回呼模式:handle_event_function
本節說明如何變更範例,以使用一個 handle_event/4
函數。先前使用的方法首先根據事件進行分支在這裡效果不佳,因為有狀態進入呼叫,因此此範例首先根據狀態進行分支。
-export([handle_event/4]).
callback_mode() ->
[handle_event_function,state_enter].
%%
%% State: locked
handle_event(enter, _OldState, locked, Data) ->
do_lock(),
{keep_state, Data#{buttons := []}};
handle_event(state_timeout, button, locked, Data) ->
{keep_state, Data#{buttons := []}};
handle_event(
internal, {button,Button}, locked,
#{code := Code, length := Length, buttons := Buttons} = Data) ->
NewButtons =
if
length(Buttons) < Length ->
Buttons;
true ->
tl(Buttons)
end ++ [Button],
if
NewButtons =:= Code -> % Correct
{next_state, open, Data};
true -> % Incomplete | Incorrect
{keep_state, Data#{buttons := NewButtons},
[{state_timeout,30_000,button}]} % Time in milliseconds
end;
%%
%% State: open
handle_event(enter, _OldState, open, _Data) ->
do_unlock(),
{keep_state_and_data,
[{state_timeout,10_000,lock}]}; % Time in milliseconds
handle_event(state_timeout, lock, open, Data) ->
{next_state, locked, Data};
handle_event(internal, {button,_}, open, _) ->
{keep_state_and_data,[postpone]};
%% Common events
handle_event(cast, {down,Button}, _State, Data) ->
{keep_state, Data#{button => Button}};
handle_event(cast, {up,Button}, _State, Data) ->
case Data of
#{button := Button} ->
{keep_state, maps:remove(button, Data),
[{next_event,internal,{button,Button}},
{state_timeout,30_000,button}]}; % Time in milliseconds
#{} ->
keep_state_and_data
end;
handle_event({call,From}, code_length, _State, #{length := Length}) ->
{keep_state_and_data,
[{reply,From,Length}]}.
請注意,將 open
狀態的按鈕延遲到 locked
狀態對於密碼鎖來說似乎是一件奇怪的事情,但它至少說明了事件延遲。
篩選狀態
本章到目前為止的範例伺服器會在錯誤日誌中列印完整的內部狀態,例如,當被退出訊號終止或因為內部錯誤時。該狀態包含密碼鎖程式碼以及仍需要解鎖的數位。
此狀態資料可以被視為敏感資料,並且可能不是您想要在錯誤日誌中看到的資料,因為某些無法預測的事件。
篩選狀態的另一個原因是狀態太大而無法列印,因為它會用無趣的詳細資訊填滿錯誤日誌。
為了避免這種情況,您可以透過實作函數 Module:format_status/2
來格式化錯誤日誌中的內部狀態,以及從 sys:get_status/1,2
返回的內部狀態,例如這樣
...
-export([init/1,terminate/3,format_status/2]).
...
format_status(Opt, [_PDict,State,Data]) ->
StateData =
{State,
maps:filter(
fun (code, _) -> false;
(_, _) -> true
end,
Data)},
case Opt of
terminate ->
StateData;
normal ->
[{data,[{"State",StateData}]}]
end.
並非強制實作 Module:format_status/2
函數。如果您不實作,則會使用預設實作,該實作與此範例函數執行相同的操作,但不篩選 Data
詞彙,也就是說,StateData = {State, Data}
,在此範例中包含敏感資訊。
複雜狀態
回呼模式 handle_event_function
允許使用非原子狀態,如 回呼模式 一節中所述,例如,像元組一樣的複雜狀態詞彙。
使用此方法的原因之一是,當您有一個狀態項目變更時,應該取消狀態逾時,或者是一個會影響事件處理以及延遲事件的項目。我們將採用後者,並透過引入可配置的鎖定按鈕(這是相關的狀態項目)來使先前的範例更加複雜,該按鈕在 open
狀態下會立即鎖定門,並且使用 API 函數 set_lock_button/1
來設定鎖定按鈕。
現在假設我們在門打開時呼叫 set_lock_button
,並且我們已經延遲了作為新鎖定按鈕的按鈕事件。
1> code_lock:start_link([a,b,c], x).
{ok,<0.666.0>}
2> code_lock:button(a).
ok
3> code_lock:button(b).
ok
4> code_lock:button(c).
ok
Open
5> code_lock:button(y).
ok
6> code_lock:set_lock_button(y).
x
% What should happen here? Immediate lock or nothing?
我們可以說按鈕按下得太早,因此不應將其識別為鎖定按鈕。或者我們可以使鎖定按鈕成為狀態的一部分,因此當我們隨後在鎖定狀態下變更鎖定按鈕時,該變更會變成狀態變更,並且會重試所有延遲的事件,因此鎖會立即鎖定!
我們將狀態定義為 {StateName, LockButton}
,其中 StateName
與之前相同,並且 LockButton
是目前的鎖定按鈕
-module(code_lock).
-behaviour(gen_statem).
-define(NAME, code_lock_3).
-export([start_link/2,stop/0]).
-export([button/1,set_lock_button/1]).
-export([init/1,callback_mode/0,terminate/3]).
-export([handle_event/4]).
start_link(Code, LockButton) ->
gen_statem:start_link(
{local,?NAME}, ?MODULE, {Code,LockButton}, []).
stop() ->
gen_statem:stop(?NAME).
button(Button) ->
gen_statem:cast(?NAME, {button,Button}).
set_lock_button(LockButton) ->
gen_statem:call(?NAME, {set_lock_button,LockButton}).
init({Code,LockButton}) ->
process_flag(trap_exit, true),
Data = #{code => Code, length => length(Code), buttons => []},
{ok, {locked,LockButton}, Data}.
callback_mode() ->
[handle_event_function,state_enter].
%% State: locked
handle_event(enter, _OldState, {locked,_}, Data) ->
do_lock(),
{keep_state, Data#{buttons := []}};
handle_event(state_timeout, button, {locked,_}, Data) ->
{keep_state, Data#{buttons := []}};
handle_event(
cast, {button,Button}, {locked,LockButton},
#{code := Code, length := Length, buttons := Buttons} = Data) ->
NewButtons =
if
length(Buttons) < Length ->
Buttons;
true ->
tl(Buttons)
end ++ [Button],
if
NewButtons =:= Code -> % Correct
{next_state, {open,LockButton}, Data};
true -> % Incomplete | Incorrect
{keep_state, Data#{buttons := NewButtons},
[{state_timeout,30_000,button}]} % Time in milliseconds
end;
%%
%% State: open
handle_event(enter, _OldState, {open,_}, _Data) ->
do_unlock(),
{keep_state_and_data,
[{state_timeout,10_000,lock}]}; % Time in milliseconds
handle_event(state_timeout, lock, {open,LockButton}, Data) ->
{next_state, {locked,LockButton}, Data};
handle_event(cast, {button,LockButton}, {open,LockButton}, Data) ->
{next_state, {locked,LockButton}, Data};
handle_event(cast, {button,_}, {open,_}, _Data) ->
{keep_state_and_data,[postpone]};
%%
%% Common events
handle_event(
{call,From}, {set_lock_button,NewLockButton},
{StateName,OldLockButton}, Data) ->
{next_state, {StateName,NewLockButton}, Data,
[{reply,From,OldLockButton}]}.
do_lock() ->
io:format("Locked~n", []).
do_unlock() ->
io:format("Open~n", []).
terminate(_Reason, State, _Data) ->
State =/= locked andalso do_lock(),
ok.
休眠
如果您在一個節點中有許多伺服器,並且它們在其生命週期中存在一些預期會閒置一段時間的狀態,並且所有這些伺服器所需的堆積記憶體量是一個問題,那麼可以透過使用 proc_lib:hibernate/3
使伺服器休眠,來最大程度地減少伺服器的記憶體佔用空間。
注意
休眠一個程序相當耗費資源;請參閱
erlang:hibernate/3
。這不是您希望在每次事件之後執行的操作。
在此範例中,我們可以在 {open, _}
狀態下休眠,因為在該狀態下通常發生的是,經過一段時間後,狀態逾時會觸發轉換為 {locked, _}
...
%%
%% State: open
handle_event(enter, _OldState, {open,_}, _Data) ->
do_unlock(),
{keep_state_and_data,
[{state_timeout,10_000,lock}, % Time in milliseconds
hibernate]};
...
進入 {open, _}
狀態時,最後一行動作列表中的原子 hibernate
是唯一變更的地方。如果任何事件進入 {open, _},
狀態,我們不會費心重新休眠,因此伺服器會在任何事件後保持喚醒狀態。
若要變更該行為,我們需要在更多地方插入動作 hibernate
。例如,與狀態無關的 set_lock_button
操作必須使用 hibernate
,但只能在 {open, _}
狀態中使用,這會使程式碼變得雜亂。
另一個常見的情境是使用 事件逾時 在一段時間不活動後觸發休眠。還有一個伺服器啟動選項 {hibernate_after, Timeout}
,適用於 start/3,4
、start_link/3,4
或 enter_loop/4,5,6
,可用於自動休眠伺服器。
此特定伺服器可能不會使用值得休眠的堆積記憶體。若要從休眠中獲得任何好處,您的伺服器必須在回呼執行期間產生不可忽略的垃圾,而此範例伺服器可以作為一個不好的例子。