非常教程

Erlang 20参考手册

指南:设计原则 | Guide: Design principles

3. gen_statem Behavior

本节将gen_statem(3)在 STDLIB 中的手册页中进行阅读,其中详细介绍了所有接口函数和回调函数。

注意

这是 Erlang / OTP 19.0 中的一个新行为。它已经过彻底审查,足够稳定,可供至少两个重要的 OTP 应用程序使用,并且将留在此处。根据用户的反馈,我们不期望但可以发现有必要对 Erlang / OTP 20.0 进行微小而非后向兼容的更改。

3.1 事件驱动的状态机

既定的自动机理论并不涉及如何触发状态转换,而是假定输出是输入(和状态)的函数,并且它们是某种类型的值。

对于事件驱动的状态机,输入是触发状态转换的事件,输出是在状态转换期间执行的动作。它可以类似于有限状态机的数学模型被描述为以下形式的一组关系:

State(S) x Event(E) -> Actions(A), State(S')

这些关系解释如下:如果我们处于状态S并且事件E发生,我们将执行操作A并转换到状态S'。注意S'可以等于S

作为AS'仅仅依赖于SE,这里描述的这种状态机是一种Mealy机器(参见例如相应的维基百科文章)。

像大多数gen_行为一样,除了状态之外,gen_statem保留一台服务器 Data因此,由于状态数量(假设有足够的虚拟机内存)或不同的输入事件数量没有限制,实际上使用此行为的状态机实际上是图灵完备的。但它的感觉大部分是一个事件驱动的 Mealy 机器。

3.2 回调模块

gen_statem行为支持两种回调模式:

  • 在模式中state_functions,状态转换规则被编写为符合以下约定的一些 Erlang 函数:StateName(EventType,EventContent,Data) - > ...此处的动作代码... {next_state,NewStateName,NewData}。这个表格在这里的大多数例子中都有使用,例如在章节中Example
  • 在模式中handle_event_function,只有一个 Erlang 函数提供了所有的状态转换规则:

handle_event(EventType,EventContent,State,Data) - > ...此处的动作代码... {next_state,NewState,NewData}

有关One Event Handler示例,请参阅部分。

这两种模式都允许其他返回元组; 看到Module:StateName/3gen_statem手册页。例如,这些其他返回元组可以停止机器,在机器引擎上执行状态转换操作,并发送回复。

选择回拨模式

这两者callback modes给出了不同的可能性和限制,但仍有一个目标:你想要处理事件和状态的所有可能的组合。

这可以通过例如关注当时的一个状态来完成,并且对于每个状态确保处理所有事件。或者,您可以将焦点放在一个事件上,并确保在每个状态下都处理它。您也可以使用这些策略的组合。

state_functions,你被限制使用原子状态,gen_statem引擎根据你的状态名称分支。这将鼓励回调模块收集特定于代码中同一位置的一个状态的所有事件操作的实现,从而集中处理当时的一个状态。

当你有一个常规的状态图时,这种模式非常合适,就像本章中描述的那样,它描述了属于一个状态的所有事件和动作,并且每个状态都有其独特的名称。

随着handle_event_function,你可以自由地混合策略,因为所有事件和状态都在相同的回调函数中处理。

当您想要关注当时的某个事件或当时的某个状态时,此模式同样适用,但功能Module:handle_event/4快速增长太大而无法分支到辅助函数。

该模式可以使用非原子状态,例如复杂状态甚至分层状态。如果,例如,状态图是对客户端和协议的服务器端主要是一样的,你可以有一个状态{StateName,server}{StateName,client},并StateName确定在代码来处理在该州大多数事件。然后使用元组的第二个元素来选择是否处理特殊的客户端或服务器端事件。

3.3 状态键入调用

只要状态发生变化,gen_statem行为就可以自动call the state callback使用特殊参数自动回调模式,以便您可以在状态转换规则的其余部分附近写入状态输入操作。它通常看起来像这样:

StateName(enter, _OldState, Data) ->
    ... code for state entry actions here ...
    {keep_state, NewData};
StateName(EventType, EventContent, Data) ->
    ... code for actions here ...
    {next_state, NewStateName, NewData}.

取决于你的状态机是如何指定的,这可能是一个非常有用的功能,但它迫使你处理状态输入所有状态的调用。另见State Entry Actions章节。

3.4 行动

在第一部分中,Event-Driven State Machines动作被提及为一般状态机模型的一部分。这些常规操作是使用回调模块gen_statem在事件处理回调函数中执行的代码返回gen_statem引擎之前执行的。

回调函数可以gen_statem在回调函数返回后命令引擎执行更多特定的状态转换操作。这些都是通过返回的列表排列actionsreturn tuplecallback function。这些状态转换操作会影响gen_statem引擎本身,并可以执行以下操作:

  • Postpone 当前事件,请参见部分 Postponing Events
  • Hibernate在... gen_statem中对接Hibernation
  • 开始一state time-out节,阅读更多内容State Time-Outs
  • 开始一generic time-out节,阅读更多内容Generic Time-Outs
  • 开始event time-out,看更多的部分Event Time-Outs
  • Reply 给部分末尾提到的调用者 All State Events
  • 生成next event要处理的内容,请参见部分Self-Generated Events

有关详细信息,请参阅gen_statem(3)手册页。例如,您可以回复多个呼叫者,生成多个下一个事件,并将超时设置为相对或绝对时间。

3.5 事件类型

事件分类不同event types。对于给定的状态,所有类型的事件都在同一个回调函数中处理,并且该函数获取EventTypeEventContent作为参数。

以下是事件类型的完整列表以及它们来自哪里:

cast由...生成gen_statem:cast{call,From}通过生成gen_statem:call,其中From是通过状态转换操作{reply,From,Msg}或通过调用回复时使用的回复地址gen_statem:replyinfo通过发送给gen_statem流程的任何常规流程消息生成。state_timeout由状态转换动作{state_timeout,Time,EventContent}状态定时器超时产生。{timeout,Name}由状态转换动作生成{{timeout,Name},Time,EventContent}通用定时器超时。timeout由状态转换动作{timeout,Time,EventContent}(或其简写形式Time)事件定时器超时生成。internal由状态转换生成action{next_event,internal,EventContent}。以上所有事件类型也可以使用生成{next_event,EventType,EventContent}

3.6 示例

带有密码锁的门可以看作是一个状态机。最初,门被锁定。当某人按下按钮时,会生成一个事件。根据之前按下的按钮,到目前为止的顺序可能是正确的,不完整的或错误的。如果正确,门将被解锁10秒(10,000毫秒)。如果不完整,我们等待另一个按钮被按下。如果错了,我们从头开始,等待一个新的按钮序列。

图3.1:代码锁定状态图

此代码锁状态机可以使用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,code_change/4]).
-export([locked/3,open/3]).

start_link(Code) ->
    gen_statem:start_link({local,?NAME}, ?MODULE, Code, []).

button(Digit) ->
    gen_statem:cast(?NAME, {button,Digit}).

init(Code) ->
    do_lock(),
    Data = #{code => Code, remaining => Code},
    {ok, locked, Data}.

callback_mode() ->
    state_functions.

locked(
  cast, {button,Digit},
  #{code := Code, remaining := Remaining} = Data) ->
    case Remaining of
        [Digit] ->
	    do_unlock(),
            {next_state, open, Data#{remaining := Code},
             [{state_timeout,10000,lock}]};
        [Digit|Rest] -> % Incomplete
            {next_state, locked, Data#{remaining := Rest}};
        _Wrong ->
            {next_state, locked, Data#{remaining := Code}}
    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.
code_change(_Vsn, State, Data, _Extra) ->
    {ok, State, Data}.
    

代码在下一节中解释。

3.7 启动 gen_statem

在上一节的示例中,gen_statem通过调用code_lock:start_link(Code)以下命令启动:

start_link(Code) ->
    gen_statem:start_link({local,?NAME}, ?MODULE, Code, []).
    

start_link调用函数gen_statem:start_link/4,它产生并链接到一个新的进程,a gen_statem

  • 第一个参数{local,?NAME}指定名称。在这种情况下,通过宏gen_statem在本地注册。如果名称被省略,则不会注册。相反,它的pid必须使用。该名称也可以指定为,然后在内核中使用该名称进行注册。code_lock?NAMEgen_statem{global,Name}gen_statemglobal:register_name/2
  • 第二个参数?MODULE是,回调模块的名称,即回调函数所在的模块,即该模块。

接口功能(start_link/1button/1)位于同一模块作为回调函数(init/1locked/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,它存储正确的按钮序列和键。remaining存储剩余的正确按钮序列(,与code从)开始。

init(Code) ->
    do_lock(),
    Data = #{code => Code, remaining => Code},
    {ok,locked,Data}.
    

功能gen_statem:start_link是同步的。它在gen_statem初始化并准备好接收事件之前不会返回。

必须使用gen_statem:start_link函数,如果它gen_statem是监督树的一部分,即由监督者启动。另一个功能,gen_statem:start可以用来启动一个独立的gen_statem,即gen_statem不是监督树的一部分。

callback_mode() ->
    state_functions.
    

在这种情况下,函数Module:callback_mode/0选择CallbackMode回调模块state_functions。也就是说,每个状态都有自己的处理函数。

3.8 处理事件

通知代码锁定有关按钮事件的功能是通过gen_statem:cast/2以下方式实现的:

button(Digit) ->
    gen_statem:cast(?NAME, {button,Digit}).
    

第一个参数是和的名称gen_statem必须与用于启动它的名称一致。所以,我们?NAME在开始时使用相同的宏。{button,Digit}是事件内容。

这个事件被写成一条消息并发送给gen_statem。当接收到事件时,这些gen_statem调用StateName(cast, Event, Data)将返回一个元组{next_state, NewStateName, NewData},或者{next_state, NewStateName, NewData, Actions}StateName是当前状态NewStateName的名称,并且是要进入的下一个状态的名称。NewData是服务器数据的新值gen_statem,并且Actionsgen_statem引擎上的操作列表。

locked(
  cast, {button,Digit},
  #{code := Code, remaining := Remaining} = Data) ->
    case Remaining of
        [Digit] -> % Complete
	    do_unlock(),
            {next_state, open, Data#{remaining := Code},
             [{state_timeout,10000,lock}]};
        [Digit|Rest] -> % Incomplete
            {next_state, locked, Data#{remaining := Rest}};
        [_|_] -> % Wrong
            {next_state, locked, Data#{remaining := Code}}
    end.

open(state_timeout, lock, Data) ->
    do_lock(),
    {next_state, locked, Data};
open(cast, {button,_}, Data) ->
    {next_state, open, Data}.
    

如果门被锁定并且按下按钮,则按下的按钮与下一个正确的按钮进行比较。根据结果​​,门被解锁并gen_statem进入状态open,或门保持在状态locked

如果按下的按钮不正确,则服务器数据将从代码序列的起始处重新开始。

如果整个代码是正确的,服务器将状态更改为open

在状态下open,按钮事件被置于相同的状态而被忽略。这也可以通过返回来完成,{keep_state, Data}或者在这种情况下,Data即使通过返回也不变keep_state_and_data

3.9 状态超时

当给出正确的代码时,门被解锁,并且以下元组从以下位置返回locked/2

{next_state, open, Data#{remaining := Code},
 [{state_timeout,10000,lock}]};
    

10,000是以毫秒为单位的超时值。在此时间(10秒)后,发生超时。然后,StateName(state_timeout, lock, Data)被调用。当门已经处于状态open10秒时,超时发生。之后,门再次锁定:

open(state_timeout, lock,  Data) ->
    do_lock(),
    {next_state, locked, Data};
    

当状态机改变状态时,状态超时定时器会自动取消。您可以通过将其设置为新的时间来重新启动状态超时,从而取消正在运行的计时器并启动新的时间。这意味着您可以通过重新启动它来取消状态超时infinity

3.10 所有状态事件

有时事件可以在任何状态下到达gen_statem。在一个通用的状态处理函数中处理这些函数是很方便的,所有的状态函数都会调用不是特定于状态的事件。

考虑一个code_length/0返回正确代码长度的函数(它不应该对揭示敏感)。我们将所有不属于特定状态的事件分派给通用函数handle_event/3

...
-export([button/1,code_length/0]).
...

code_length() ->
    gen_statem:call(?NAME, code_length).

...
locked(...) -> ... ;
locked(EventType, EventContent, Data) ->
    handle_event(EventType, EventContent, Data).

...
open(...) -> ... ;
open(EventType, EventContent, Data) ->
    handle_event(EventType, EventContent, Data).

handle_event({call,From}, code_length, #{code := Code} = Data) ->
    {keep_state, Data, [{reply,From,length(Code)}]}.
    

这个例子使用gen_statem:call/2,它等待服务器的回复。回复与保留当前状态{reply,From,Reply}{keep_state, ...}元组中的操作列表中的元组一起发送。当您想要保持当前状态,但不知道或关心它是什么时,此返回表单很方便。

3.11 一个事件处理程序

如果使用模式handle_event_function,所有的事件都会被处理Module:handle_event/4,我们可以(但不一定)使用以事件为中心的方法,我们首先根据事件分支,然后依赖于状态:

...
-export([handle_event/4]).

...
callback_mode() ->
    handle_event_function.

handle_event(cast, {button,Digit}, State, #{code := Code} = Data) ->
    case State of
	locked ->
	    case maps:get(remaining, Data) of
		[Digit] -> % Complete
		    do_unlock(),
		    {next_state, open, Data#{remaining := Code},
                     [{state_timeout,10000,lock}]};
		[Digit|Rest] -> % Incomplete
		    {keep_state, Data#{remaining := Rest}};
		[_|_] -> % Wrong
		    {keep_state, Data#{remaining := Code}}
	    end;
	open ->
            keep_state_and_data
    end;
handle_event(state_timeout, lock, open, Data) ->
    do_lock(),
    {next_state, locked, Data}.

...
    

3.12 停止

在监督树中

如果它gen_statem是监督树的一部分,则不需要停止功能。该gen_statem自动由监管当局终止。具体如何完成是由shutdown strategy主管人员定义的。

如果在终止之前需要清理,则关闭策略必须是超时值,并且gen_statem函数中的must必须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,最好通过API函数停止使用它:

...
-export([start_link/1,stop/0]).

...
stop() ->
    gen_statem:stop(?NAME).
      

这使得gen_statem调用回调函数terminate/3就像监督服务器一样,并等待进程终止。

3.13事件暂停

从继承了超时功能gen_statem的前身gen_fsm,是一个事件超时,也就是说,如果一个事件到达定时器被取消。你得到一个事件或超时,但不是两个。

它由状态转换动作来排序{timeout,Time,EventContent},或者仅仅是Time,或者甚至仅仅Time是一个动作列表(后者是一个继承的形式)gen_fsm

这种类型的超时对于例如处于非活动状态非常有用。如果没有按下任何按钮,例如30秒,让我们重新启动代码序列:

...

locked(
  timeout, _, 
  #{code := Code, remaining := Remaining} = Data) ->
    {next_state, locked, Data#{remaining := Code}};
locked(
  cast, {button,Digit},
  #{code := Code, remaining := Remaining} = Data) ->
...
        [Digit|Rest] -> % Incomplete
            {next_state, locked, Data#{remaining := Rest}, 30000};
...
     

无论何时我们收到一个按钮事件,我们都会启动一个30秒的事件超时,如果我们得到一个事件类型,timeout我们会重置剩余的代码序列。

事件超时被任何其他事件取消,因此您可能会收到其他事件或超时事件。因此不可能也不需要取消或重新启动事件超时。无论您采取何种行动已经取消了事件超时...

3.14通用超时

前面的状态超时示例仅在状态机在超时期间保持相同状态时才起作用。事件超时仅在没有干扰的无关事件发生时才起作用。

您可能希望在一种状态下启动计时器,并在另一种状态下响应超时,也可以在不更改状态的情况下取消超时,或者可能并行运行多个超时。所有这些都可以用generic time-outs。它们可能看起来有点像,event time-outs但包含一个名称以允许同时允许其中的任意数量,并且它们不会自动取消。

以下是如何通过改为使用以下名称的通用超时来完成上一个示例中的状态超时open_tm

...
locked(
  cast, {button,Digit},
  #{code := Code, remaining := Remaining} = Data) ->
    case Remaining of
        [Digit] ->
	    do_unlock(),
            {next_state, open, Data#{remaining := Code},
	     [{{timeout,open_tm},10000,lock}]};
...

open({timeout,open_tm}, lock, Data) ->
    do_lock(),
    {next_state,locked,Data};
open(cast, {button,_}, Data) ->
    {keep_state,Data};
...
    

就像state time-outs你可以重新启动或取消特定的通用超时一样,只需将它设置为新的时间或infinity

处理迟到超时的另一种方法是不取消,但如果它到达已知迟到的状态则忽略它。

3.15 Erlang定时器

处理超时的最通用的方式是使用Erlang定时器; 见erlang:start_timer3,4。大多数超时任务都可以使用超时功能执行gen_statem,但是一个例子是,如果您需要返回值erlang:cancel_timer(Tref),那是不可能的。计时器的剩余时间。

以下是如何通过改为使用Erlang定时器来完成前一个示例中的状态超时:

...
locked(
  cast, {button,Digit},
  #{code := Code, remaining := Remaining} = Data) ->
    case Remaining of
        [Digit] ->
	    do_unlock(),
	    Tref = erlang:start_timer(10000, self(), lock),
            {next_state, open, Data#{remaining := Code, 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};
...
    

timer当我们更改为状态时,从地图中除去密钥locked并不是绝对必要的,因为我们只能open使用更新的timer地图值进入状态。但在州内没有过时的价值可能会很好Data

如果因为其他事件需要取消计时器,则可以使用erlang:cancel_timer(Tref)。请注意,在此之后,超时消息无法到达,除非您之前已将其延期(请参阅下一节),因此请确保不会意外推迟此类消息。另外请注意,超时消息可能在您取消之前已经到达,因此您可能需要根据返回值从过程邮箱读出这样的消息erlang:cancel_timer(Tref)

处理迟到超时的另一种方法是不取消它,但如果它到达已知迟到的状态则忽略它。

3.16推迟活动

如果您想忽略当前状态中的特定事件并在将来状态下处理它,则可以推迟该事件。推迟的事件在州改变后重试,也就是说OldState =/= NewState

推迟是由状态转换排序的action postpone

在这个例子中open,我们可以推迟它们,而不是在状态中忽略按钮事件,而是在状态中排队并稍后处理locked

...
open(cast, {button,_}, Data) ->
    {keep_state,Data,[postpone]};
...
    

由于延迟事件仅在状态更改后重试,因此您必须考虑在哪里保留状态数据项。您可以将其保存在服务器中Data或其State本身中,例如通过使两个或多个相同的状态保持布尔值,或通过使用复杂状态callback mode handle_event_function。如果值的变化改变了处理的事件集合,那么该值应该保存在状态中。否则,由于只有服务器数据发生更改,因此不会重试延期的事件。

如果您不推迟事件,这并不重要。但是如果你后来决定开始推迟一些事件,那么当它们应该是没有独立状态的设计缺陷时,可能会变成一个很难发现的错误。

模糊状态图

状态图不指定如何处理图中特定状态未示出的事件并不少见。希望这是在相关文本或上下文中描述的。

可能的操作:忽略该事件(可能记录它)或在其他某些状态下处理该事件,如推迟该事件。

选择性接收

Erlang的选择性接收语句通常用于在简单的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, Code)
      end).

button(Digit) ->
    ?NAME ! {button,Digit}.

locked(Code, [Digit|Remaining]) ->
    receive
	{button,Digit} when Remaining =:= [] ->
	    do_unlock(),
	    open(Code);
	{button,Digit} ->
	    locked(Code, Remaining);
	{button,_} ->
	    locked(Code, Code)
    end.

open(Code) ->
    receive
    after 10000 ->
	    do_lock(),
	    locked(Code, Code)
    end.

do_lock() ->
    io:format("Locked~n", []).
do_unlock() ->
    io:format("Open~n", []).
    

在这种情况下,选择性接收隐含open地推迟到该locked状态的任何事件。

选择性接收不能gen_statem用于任何gen_*行为,因为receive语句在gen_*引擎本身内。它必须在那里,因为所有sys兼容行为都必须对系统消息作出响应,因此在引擎接收循环中执行该操作,将非系统消息传递到回调模块。

状态转换action postpone旨在模拟选择性接收。选择性接收隐式推迟所有未收到的事件,但postpone状态转换操作会明确推迟接收到的一个事件。

两种机制具有相同的理论时间和记忆复杂度,而选择性接收语言结构具有较小的常数因子。

3.17 状态Entry行为

假设你有一个使用状态输入操作的状态机规范。尽管你可以使用自生成的事件对代码进行编码(下一节将会介绍),特别是如果只有一个或几个状态已经有了状态输入操作,这对于内置代码来说是一个完美的用例state enter calls

你返回一个包含state_entercallback_mode/0函数的列表,当状态改变时,gen_statem引擎会使用参数调用你的状态回调(enter, OldState, ...)。那么你只需要在所有状态下处理这些类似事件的调用。

...
init(Code) ->
    process_flag(trap_exit, true),
    Data = #{code => Code},
    {ok, locked, Data}.

callback_mode() ->
    [state_functions,state_enter].

locked(enter, _OldState, Data) ->
    do_lock(),
    {keep_state,Data#{remaining => Code}};
locked(
  cast, {button,Digit},
  #{code := Code, remaining := Remaining} = Data) ->
    case Remaining of
        [Digit] ->
	    {next_state, open, Data};
...

open(enter, _OldState, _Data) ->
    do_unlock(),
    {keep_state_and_data, [{state_timeout,10000,lock}]};
open(state_timeout, lock, Data) ->
    {next_state, locked, Data};
...
    

您可以通过返回的一个重复的状态入口代码{repeat_state, ...}{repeat_state_and_data,_}repeat_state_and_data以其他方式表现完全像自己的keep_state兄弟姐妹。请参阅state_callback_result()参考手册中的类型。

3.18 自生事件

有时能够将事件生成到自己的状态机有时会很有用。这可以通过状态转换完成action {next_event,EventType,EventContent}

您可以生成任何现有的事件type,但internal类型只能通过操作生成next_event。因此,它不能来自外部来源,因此您可以确定internal事件是从状态机到自身的事件。

其中一个例子是预处理传入数据,例如解密块或收集字符到换行符。纯粹主义者可能会争辩说,这应该用一个单独的状态机来模拟,这个状态机将预处理的事件发送到主状态机。但是为了减少开销,可以在主状态机的公共状态事件处理中使用一些状态数据变量来实现小型预处理状态机,然后将这些预处理事件作为内部事件发送到主状态机。

以下示例使用一个输入模型,在该输入模型中给予锁定字符,put_chars(Chars)然后调用enter()以完成输入。

...
-export(put_chars/1, enter/0).
...
put_chars(Chars) when is_binary(Chars) ->
    gen_statem:call(?NAME, {chars,Chars}).

enter() ->
    gen_statem:call(?NAME, enter).

...

locked(enter, _OldState, Data) ->
    do_lock(),
    {keep_state,Data#{remaining => Code, buf => []}};
...

handle_event({call,From}, {chars,Chars}, #{buf := Buf} = Data) ->
    {keep_state, Data#{buf := [Chars|Buf],
     [{reply,From,ok}]};
handle_event({call,From}, enter, #{buf := Buf} = Data) ->
    Chars = unicode:characters_to_binary(lists:reverse(Buf)),
    try binary_to_integer(Chars) of
        Digit ->
            {keep_state, Data#{buf := []},
             [{reply,From,ok},
              {next_event,internal,{button,Chars}}]}
    catch
        error:badarg ->
            {keep_state, Data#{buf := []},
             [{reply,From,{error,not_an_integer}}]}
    end;
...
    

如果你开始这个程序,code_lock:start([17])你可以解锁code_lock:put_chars(<<"001">>), code_lock:put_chars(<<"7">>), code_lock:enter()

3.19例修订

本节包含大部分提到的修改后的示例,还有一些使用了state enter调用,这些调用值得一个新的状态图:

图3.2:代码锁定状态图的修改

请注意,此状态图没有指定如何处理状态中的按钮事件。open因此,您需要在其他地方读取未指定的事件必须被忽略,就像在未使用的事件中一样,但在其他状态下必须处理。此外,状态图没有显示code_length/0每个州都必须处理电话。

回调模式:状态[医]功能

使用状态函数:

-module(code_lock).
-behaviour(gen_statem).
-define(NAME, code_lock_2).

-export([start_link/1,stop/0]).
-export([button/1,code_length/0]).
-export([init/1,callback_mode/0,terminate/3,code_change/4]).
-export([locked/3,open/3]).

start_link(Code) ->
    gen_statem:start_link({local,?NAME}, ?MODULE, Code, []).
stop() ->
    gen_statem:stop(?NAME).

button(Digit) ->
    gen_statem:cast(?NAME, {button,Digit}).
code_length() ->
    gen_statem:call(?NAME, code_length).

init(Code) ->
    process_flag(trap_exit, true),
    Data = #{code => Code},
    {ok, locked, Data}.

callback_mode() ->
    [state_functions,state_enter].

locked(enter, _OldState, #{code := Code} = Data) ->
    do_lock(),
    {keep_state, Data#{remaining => Code}};
locked(
  timeout, _, 
  #{code := Code, remaining := Remaining} = Data) ->
    {keep_state, Data#{remaining := Code}};
locked(
  cast, {button,Digit},
  #{code := Code, remaining := Remaining} = Data) ->
    case Remaining of
        [Digit] -> % Complete
            {next_state, open, Data};
        [Digit|Rest] -> % Incomplete
            {keep_state, Data#{remaining := Rest}, 30000};
        [_|_] -> % Wrong
            {keep_state, Data#{remaining := Code}}
    end;
locked(EventType, EventContent, Data) ->
    handle_event(EventType, EventContent, Data).

open(enter, _OldState, _Data) ->
    do_unlock(),
    {keep_state_and_data, [{state_timeout,10000,lock}]};
open(state_timeout, lock, Data) ->
    {next_state, locked, Data};
open(cast, {button,_}, _) ->
    {keep_state_and_data, [postpone]};
open(EventType, EventContent, Data) ->
    handle_event(EventType, EventContent, Data).

handle_event({call,From}, code_length, #{code := Code}) ->
    {keep_state_and_data, [{reply,From,length(Code)}]}.

do_lock() ->
    io:format("Locked~n", []).
do_unlock() ->
    io:format("Open~n", []).

terminate(_Reason, State, _Data) ->
    State =/= locked andalso do_lock(),
    ok.
code_change(_Vsn, State, Data, _Extra) ->
    {ok,State,Data}.
      

回调模式:handle_event_function

本节介绍在示例中要使用一个handle_event/4函数需要更改的内容。先前使用的第一个分支取决于事件的方法在这里并不奏效,因为状态输入了调用,所以这个例子首先根据状态分支:

...
-export([handle_event/4]).

...
callback_mode() ->
    [handle_event_function,state_enter].

%% State: locked
handle_event(
  enter, _OldState, locked,
  #{code := Code} = Data) ->
    do_lock(),
    {keep_state, Data#{remaining => Code}};
handle_event(
  timeout, _, locked,
  #{code := Code, remaining := Remaining} = Data) ->
    {keep_state, Data#{remaining := Code}};
handle_event(
  cast, {button,Digit}, locked,
  #{code := Code, remaining := Remaining} = Data) ->
    case Remaining of
        [Digit] -> % Complete
            {next_state, open, Data};
        [Digit|Rest] -> % Incomplete
            {keep_state, Data#{remaining := Rest}, 30000};
        [_|_] -> % Wrong
            {keep_state, Data#{remaining := Code}}
    end;
%%
%% State: open
handle_event(enter, _OldState, open, _Data) ->
    do_unlock(),
    {keep_state_and_data, [{state_timeout,10000,lock}]};
handle_event(state_timeout, lock, open, Data) ->
    {next_state, locked, Data};
handle_event(cast, {button,_}, open, _) ->
    {keep_state_and_data,[postpone]};
%%
%% Any state
handle_event({call,From}, code_length, _State, #{code := Code}) ->
    {keep_state_and_data, [{reply,From,length(Code)}]}.

...
      

请注意,将按钮从locked状态推迟到open状态对于代码锁来说感觉像是一件奇怪的事情,但它至少说明了事件推迟。

3.20过滤状态

本章到目前为止,示例服务器会在错误日志中打印完整的内部状态,例如,当被退出信号终止或由于内部错误。此状态包含代码锁定代码和保留解锁的数字。

这种状态数据可能被认为是敏感的,并且可能不是您想要的错误日志中的某些不可预知的事件。

过滤状态的另一个原因可能是状态太大而无法打印,因为它会用无趣的细节填充错误日志。

为了避免这种情况,可以格式化获取错误日志并sys:get_status/1,2通过实现函数返回的内部状态,Module:format_status/2例如:

...
-export([init/1,terminate/3,code_change/4,format_status/2]).
...

format_status(Opt, [_PDict,State,Data]) ->
    StateData =
	{State,
	 maps:filter(
	   fun (code, _) -> false;
	       (remaining, _) -> false;
	       (_, _) -> true
	   end,
	   Data)},
    case Opt of
	terminate ->
	    StateData;
	normal ->
	    [{data,[{"State",StateData}]}]
    end.
    

实现一个Module:format_status/2函数并不是强制性的。如果不这样做,则使用一个默认实现,它与此示例函数的作用相同,而不过滤该Data术语,即StateData = {State,Data}在此示例中包含敏感信息。

3.21复态

回调模式handle_event_function可以使用部分中所述的非原子状态Callback Modes,例如像元组这样的复杂状态项。

使用它的一个原因是当你有一个状态项目,当被改变时应该取消该状态项目state time-out,或者与延期事件一起影响事件处理。我们将通过引入一个可配置的锁定按钮(这是有问题的状态项)来复杂化前一个示例,该按钮在该open状态下立即锁定门,以及set_lock_button/1用于设置锁定按钮的API函数。

现在假设我们set_lock_button在门打开的时候打电话,并且已经推迟了一个按钮事件,直到现在还没有锁按钮。明智的是可以说这个按钮太早被按下,所以它不会被识别为锁定按钮。然而,那么现在就是锁按钮事件的按钮事件在状态转换到之后立即到达(如重试推迟),这可能会令人惊讶locked

所以我们button/1通过使用gen_statem:call并且仍然推迟其open状态中的事件来使该功能同步。然后button/1open状态期间的呼叫不会返回,直到状态转换为locked,因为它在那里处理事件并发送回复。

如果一个进程现在调用set_lock_button/1更改锁定按钮,而另一个进程挂入button/1新的锁定按钮,则可以预期悬挂锁定按钮调用立即生效并锁定该锁定。因此,我们使当前锁定按钮成为状态的一部分,以便当我们更改锁定按钮时,状态会更改并重试所有推迟的事件。

我们将状态定义为{StateName,LockButton}StateName与以前一样,并且LockButton是当前锁定按钮:

-module(code_lock).
-behaviour(gen_statem).
-define(NAME, code_lock_3).

-export([start_link/2,stop/0]).
-export([button/1,code_length/0,set_lock_button/1]).
-export([init/1,callback_mode/0,terminate/3,code_change/4,format_status/2]).
-export([handle_event/4]).

start_link(Code, LockButton) ->
    gen_statem:start_link(
        {local,?NAME}, ?MODULE, {Code,LockButton}, []).
stop() ->
    gen_statem:stop(?NAME).

button(Digit) ->
    gen_statem:call(?NAME, {button,Digit}).
code_length() ->
    gen_statem:call(?NAME, code_length).
set_lock_button(LockButton) ->
    gen_statem:call(?NAME, {set_lock_button,LockButton}).

init({Code,LockButton}) ->
    process_flag(trap_exit, true),
    Data = #{code => Code, remaining => undefined},
    {ok, {locked,LockButton}, Data}.

callback_mode() ->
    [handle_event_function,state_enter].

handle_event(
  {call,From}, {set_lock_button,NewLockButton},
  {StateName,OldLockButton}, Data) ->
    {next_state, {StateName,NewLockButton}, Data,
     [{reply,From,OldLockButton}]};
handle_event(
  {call,From}, code_length,
  {_StateName,_LockButton}, #{code := Code}) ->
    {keep_state_and_data,
     [{reply,From,length(Code)}]};
%%
%% State: locked
handle_event(
  EventType, EventContent,
  {locked,LockButton}, #{code := Code, remaining := Remaining} = Data) ->
    case {EventType, EventContent} of
	{enter, _OldState} ->
	    do_lock(),
	    {keep_state, Data#{remaining := Code}};
        {timeout, _} ->
            {keep_state, Data#{remaining := Code}};
	{{call,From}, {button,Digit}} ->
	    case Remaining of
		[Digit] -> % Complete
		    {next_state, {open,LockButton}, Data,
		     [{reply,From,ok}]};
		[Digit|Rest] -> % Incomplete
		    {keep_state, Data#{remaining := Rest, 30000},
		     [{reply,From,ok}]};
		[_|_] -> % Wrong
		    {keep_state, Data#{remaining := Code},
		     [{reply,From,ok}]}
	    end
    end;
%%
%% State: open
handle_event(
  EventType, EventContent,
  {open,LockButton}, Data) ->
    case {EventType, EventContent} of
	{enter, _OldState} ->
	    do_unlock(),
	    {keep_state_and_data, [{state_timeout,10000,lock}]};
	{state_timeout, lock} ->
	    {next_state, {locked,LockButton}, Data};
	{{call,From}, {button,Digit}} ->
	    if
		Digit =:= LockButton ->
		    {next_state, {locked,LockButton}, Data,
		     [{reply,From,locked}]};
		true ->
		    {keep_state_and_data,
		     [postpone]}
	    end
    end.

do_lock() ->
    io:format("Locked~n", []).
do_unlock() ->
    io:format("Open~n", []).

terminate(_Reason, State, _Data) ->
    State =/= locked andalso do_lock(),
    ok.
code_change(_Vsn, State, Data, _Extra) ->
    {ok,State,Data}.
format_status(Opt, [_PDict,State,Data]) ->
    StateData =
	{State,
	 maps:filter(
	   fun (code, _) -> false;
	       (remaining, _) -> false;
	       (_, _) -> true
	   end,
	   Data)},
    case Opt of
	terminate ->
	    StateData;
	normal ->
	    [{data,[{"State",StateData}]}]
    end.
    

它可能是一个不适合的物理代码锁的模型,这个button/1调用可以挂起直到锁被锁定。但对于一般的API来说并不奇怪。

3.22冬眠

如果一个节点中有许多服务器,并且它们在其生命周期中有一些状态(这些状态可能会使服务器空闲一段时间),并且所有这些服务器所需的堆内存量都是问题,那么内存占用量的服务器可以通过休眠来实现proc_lib:hibernate/3

休眠一个进程相当昂贵;见erlang:hibernate/3。这不是每个事件之后你想要做的事情。

在这个例子中,我们可以在这个状态下休眠{open,_},因为通常在该状态下发生的事情是,一段时间后的状态超时会触发一个转换{locked,_}

...
%% State: open
handle_event(
  EventType, EventContent,
  {open,LockButton}, Data) ->
    case {EventType, EventContent} of
        {enter, _OldState} ->
            do_unlock(),
            {keep_state_and_data,
             [{state_timeout,10000,lock},hibernate]};
...
    

hibernate进入{open,_}状态时最后一行的动作列表中的原子是唯一的变化。如果有任何事件到达该{open,_},州,我们不打扰其重复,因此服务器在任何事件后都保持清醒。

为了改变这一点,我们需要hibernate在更多地方插入行动。例如,为国家独立set_lock_buttoncode_length那然后就必须要知道使用的操作hibernate的同时,{open,_}状态,这会弄乱代码。

另一个并不罕见的情况是在一段时间不活动后使用事件超时来唤醒休眠。

这台服务器可能不会使用值得冬眠的堆内存。为了从休眠中获得任何东西,服务器在回调执行期间必须产生一些垃圾,对于这个示例服务器可以作为一个不好的例子。

Erlang 20

Erlang 是一种通用的面向并发的编程语言,可应付大规模开发活动的程序设计语言和运行环境。

主页 https://www.erlang.org/
源码 https://github.com/erlang/otp
版本 20
发布版本 20.1