Erlang/Elixir: 有限状态机原理笔记

developerworks

OTP 原理

有限状态机被描述为如下形式的一组关系集合.

有限状态机被描述为如下形式的一组关系集合

含义可以解释为:
如果在状态S的时候发生了事件E, 那么执行动作A并且使状态S过渡( transition )到状态S'.

对于使用 gen_fsm 行为的有限状态机来说, 状态过渡规则被实现为一些 Erlang 函数, 他们遵循如下的约定:

StateName(Event, StateData) ->
    .. code for actions here ...
    {next_state, StateName', StateData'}

对于这种形式的函数, 状态机存在多少个状态, 就应该定义多少个这样的状态处理函数

初始化回调

初始化回调

当使用 gen_fsm:start/3,4gen_fsm:start_link/3,4 启动的时候, 该函数被新的进程调用来初始化. 如果初始化成功, 该函数应该返回 {ok,StateName,StateData}, {ok,StateName,StateData,Timeout} or {ok,StateName,StateData,hibernate}, 其中 StateName 为该状态机的初始状态名, StateData 为该状态机的初始状态数据. 如果提供了一个整数超时值, 当在此时间范围内没有接受到任何消息时, 触发一个超时. 超时以原子 timeout 标识, 并且应当被 Module:StateName/2 回调函数处理. 原子 infinity 表示无线超时值, 这是默认的.

事件的产生

事件的产生

当一个 gen_fsm 进程接收到使用 gen_fsm:send_all_state_event/2 发送的一个事件时, 该回调函数被调用来处理该事件. StateNamegen_fsm 的当前状态名称. 该函数接收3个参数, 分别是事件名称(term), 状态名称(atom) 和一个状态数据(term), 返回一个结果(term)

结果包括4种:

{next_state,NextStateName,NewStateData}             # 进入下一个状态
{next_state,NextStateName,NewStateData,Timeout}     # 带超时设置进入下一个状态
{next_state,NextStateName,NewStateData,hibernate}   # 进入下一个状态并休眠
{stop,Reason,NewStateData}                          # 停止, 并调用terminate/3, 终止状态机进程

事件的产生

异步地发送一个事件给 FsmRef状态机进程, 并立即返回 ok. 状态机进程调用 Module:handle_event/3 来处理该事件.

参数说明请参考 send_event/2

send_eventsend_all_state_event 的区别是: 哪一个事件处理函数来处理事件. 当每个状态以相同的方式处理的时候, 使用该函数发送事件, 并且只需要一个handle_event子句, 而不需要每一个状态名函数都需要一个handle_event子句.

事件的处理

事件的处理

对于每一个可能的状态都应该有一个这样的函数实例. 当 gen_fsm 使用 gen_fsm:send_event/2发出的一个事件时, 该函数的一个和当前状态名相同的同名函数实例被调用来处理这个事件. 如果发生超时也可以被调用.

如果发生超时, Event 是一个原子 timeout, 否则为传递给 send_event/2 的参数.

StateDatagen_fsm 的状态数据.

如果函数返回 {next_state, NextStateName, NewStateData}, {next_state, NextStateName, NewStateData, Timeout}, {next_state, NextStateName, NewStateData, hibernate}`状态机继续执行

启动状态机进程

start_link(Module, Args, Options) -> Result
start_link(FsmName, Module, Args, Options) -> Result

Option = {debug,Dbgs} | {timeout,Time} | {spawn_opt,SOpts}

关于选项, 如果给定了 {timeout,Time} 参数, 状态机初始化必须在 Time 毫秒内完成, 否则, 进程终止, 启动函数返回 {error,timeout} 错误

如果成功初始化, 该函数返回 {ok,Pid}, 其中 Pid 为该状态机进程的进程ID. 如果已经存在一个名为 FsmName 的进程, 函数返回 {error, {already_started, Pid}}

实践

我们这里使用Elixir作为示例来演示如何创建一个状态机来解决实际的问题. 这里我要解决的问题是把服务器端处理网络协议的进程归纳为在多个状态之间过渡的这么一个状态机.

创建项目

第一步: 创建一个项目

➜  /tmp mix new ex_fsm_example --sup
* creating README.md
* creating .gitignore
* creating mix.exs
* creating config
* creating config/config.exs
* creating lib
* creating lib/ex_fsm_example.ex
* creating test
* creating test/test_helper.exs
* creating test/ex_fsm_example_test.exs

Your Mix project was created successfully.
You can use "mix" to compile it, test it, and more:

    cd ex_fsm_example
    mix test

Run "mix help" for more commands.

第二步: 创建子目录, 并增加一个模块

cd ex_fsm_example/lib
mkdir ex_fsm_example
cd ex_fsm_example
touch worker.ex

第三步: ExFsmExample.Worker 基本实现

defmodule ExFsmExample.Worker do
  @behaviour :gen_fsm
  def start_link() do
    :gen_fsm.start_link({:local, __MODULE__}, __MODULE__, [], [])
  end
end

第四步: 编译

mix compile
lib/ex_fsm_.../worker.ex:1: warning: undefined behaviour function code_change/4 (for behaviour :gen_fsm)
lib/ex_fsm_.../worker.ex:1: warning: undefined behaviour function handle_event/3 (for behaviour :gen_fsm)
lib/ex_fsm_.../worker.ex:1: warning: undefined behaviour function handle_info/3 (for behaviour :gen_fsm)
lib/ex_fsm_.../worker.ex:1: warning: undefined behaviour function handle_sync_event/4 (for behaviour :gen_fsm)
lib/ex_fsm_.../worker.ex:1: warning: undefined behaviour function init/1 (for behaviour :gen_fsm)
lib/ex_fsm_.../worker.ex:1: warning: undefined behaviour function terminate/3 (for behaviour :gen_fsm)
Compiled lib/ex_fsm_example/worker.ex

输出告诉我们, gen_fsm 行为的哪些函数还么有实现. 依次添加函数实现即可.

第五步: 启动,并测试

模块基本结构

defmodule ExFsmExample.Worker do
  @behaviour :gen_fsm

  def start_link() do
    :gen_fsm.start_link({:local, __MODULE__}, __MODULE__, [], [])
  end

  def init(_args) do
    state = %{socket: :undefined}
    {:ok, :on, state}
  end

  def handle_event(event, state_name, state_data) do
    {:next_state, state_name, state_data}
  end

  def handle_sync_event(event, from, state_name, state_data) do
    {:next_state, state_name, state_data}
  end

  def handle_info(info, state_name, state_data) do
    {:next_state, state_name, state_data}
  end

  def terminate(reason, state_name, state_data) do
    nil
  end

  def code_change(_old_vsn, state_name, state_data, _extra) do
    {:ok, state_name, state_data}
  end
end

有限状态机的进程信息

有限状态机的进程信息

一个门禁的例子

这个例子是根据 code_lock 用 Elixir 重写的

require Logger
defmodule ExFsmExample.CodeLock do
  @moduledoc """
  一个经典的密码锁状态机.
  应用场景:
  1. 比如出入办公室的自动门, 输入密码门打开, 10秒钟后自动关闭
  """

  @doc """
  用一个密码初始化这个状态机, 反转密码的顺序
  """
  def start_link(password) do
    Logger.debug "门禁的密码为: #{inspect password}"
    :gen_fsm.start_link({:local, __MODULE__}, __MODULE__, Enum.reverse(password), [])
  end

  def button(digit) do
    Logger.debug "您输入了 #{digit}"
    :gen_fsm.send_event(__MODULE__, {:button, digit})
  end

  @doc """
  初始化状态包含一个字符输入队列, 和一个密码作为初始状态
  """
  def init(password) do
    Logger.debug "密码的逆序值为: #{inspect password}"
    {:ok, :locked, {[], password}}
  end

  @doc """
  当外部调用button/1函数输入数字的时候, 执行这个状态函数
  """
  def locked({:button, digit}, {sofar, password}) do
    now = [digit | sofar]
    Logger.debug "Now: #{inspect now}, password #{inspect password}"
    case [digit | sofar] do
      ^password ->
        # do_unlock()
        Logger.debug "已打开, 3秒后自动关闭"
        {:next_state, :open, {[], password}, 3000}
      incomplete when length(incomplete) < length(password) ->
        Logger.debug "#{inspect incomplete}"
        {:next_state, :locked, {incomplete, password}}
      _wrong ->
        Logger.debug "密码错误"
        {:next_state, :locked, {[], password}}
    end
  end

  def open(:timeout, state) do
    # do_lock()
    Logger.debug "超时, 自动关闭"
    {:next_state, :locked, state}
  end
end
阅读 3.5k

Erlang/Elixir/Java/Javascript实践
本专栏是一个主要研究Erlang/Elixir语言的专栏. 附带其他相关的, 和不相关的东西, 目的是记录自己的学习...
1.6k 声望
256 粉丝
0 条评论
你知道吗?

1.6k 声望
256 粉丝
宣传栏