热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

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

OTP原理有限状态机被描述为如下形式的一组关系集合.含义可以解释为:如果在状态S的时候发生了事件E,那么执行动作A并且使状态S过渡(transition)到状态S.对于使用gen_

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.exsYour Mix project was created successfully.
You can use "mix" to compile it, test it, and more:cd ex_fsm_examplemix testRun "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_fsmdef 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_fsmdef start_link() do:gen_fsm.start_link({:local, __MODULE__}, __MODULE__, [], [])enddef init(_args) dostate = %{socket: :undefined}{:ok, :on, state}enddef handle_event(event, state_name, state_data) do{:next_state, state_name, state_data}enddef handle_sync_event(event, from, state_name, state_data) do{:next_state, state_name, state_data}enddef handle_info(info, state_name, state_data) do{:next_state, state_name, state_data}enddef terminate(reason, state_name, state_data) donilenddef 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) doLogger.debug "门禁的密码为: #{inspect password}":gen_fsm.start_link({:local, __MODULE__}, __MODULE__, Enum.reverse(password), [])enddef button(digit) doLogger.debug "您输入了 #{digit}":gen_fsm.send_event(__MODULE__, {:button, digit})end@doc """初始化状态包含一个字符输入队列, 和一个密码作为初始状态"""def init(password) doLogger.debug "密码的逆序值为: #{inspect password}"{:ok, :locked, {[], password}}end@doc """当外部调用button/1函数输入数字的时候, 执行这个状态函数"""def locked({:button, digit}, {sofar, password}) donow = [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) Logger.debug "#{inspect incomplete}"{:next_state, :locked, {incomplete, password}}_wrong ->Logger.debug "密码错误"{:next_state, :locked, {[], password}}endenddef open(:timeout, state) do# do_lock()Logger.debug "超时, 自动关闭"{:next_state, :locked, state}end
end



推荐阅读
  • Nginx使用AWStats日志分析的步骤及注意事项
    本文介绍了在Centos7操作系统上使用Nginx和AWStats进行日志分析的步骤和注意事项。通过AWStats可以统计网站的访问量、IP地址、操作系统、浏览器等信息,并提供精确到每月、每日、每小时的数据。在部署AWStats之前需要确认服务器上已经安装了Perl环境,并进行DNS解析。 ... [详细]
  • 本文介绍了RPC框架Thrift的安装环境变量配置与第一个实例,讲解了RPC的概念以及如何解决跨语言、c++客户端、web服务端、远程调用等需求。Thrift开发方便上手快,性能和稳定性也不错,适合初学者学习和使用。 ... [详细]
  • 在重复造轮子的情况下用ProxyServlet反向代理来减少工作量
    像不少公司内部不同团队都会自己研发自己工具产品,当各个产品逐渐成熟,到达了一定的发展瓶颈,同时每个产品都有着自己的入口,用户 ... [详细]
  • imx6ull开发板驱动MT7601U无线网卡的方法和步骤详解
    本文详细介绍了在imx6ull开发板上驱动MT7601U无线网卡的方法和步骤。首先介绍了开发环境和硬件平台,然后说明了MT7601U驱动已经集成在linux内核的linux-4.x.x/drivers/net/wireless/mediatek/mt7601u文件中。接着介绍了移植mt7601u驱动的过程,包括编译内核和配置设备驱动。最后,列举了关键词和相关信息供读者参考。 ... [详细]
  • 利用Visual Basic开发SAP接口程序初探的方法与原理
    本文介绍了利用Visual Basic开发SAP接口程序的方法与原理,以及SAP R/3系统的特点和二次开发平台ABAP的使用。通过程序接口自动读取SAP R/3的数据表或视图,在外部进行处理和利用水晶报表等工具生成符合中国人习惯的报表样式。具体介绍了RFC调用的原理和模型,并强调本文主要不讨论SAP R/3函数的开发,而是针对使用SAP的公司的非ABAP开发人员提供了初步的接口程序开发指导。 ... [详细]
  • 本文介绍了在Linux下安装和配置Kafka的方法,包括安装JDK、下载和解压Kafka、配置Kafka的参数,以及配置Kafka的日志目录、服务器IP和日志存放路径等。同时还提供了单机配置部署的方法和zookeeper地址和端口的配置。通过实操成功的案例,帮助读者快速完成Kafka的安装和配置。 ... [详细]
  • 一、什么是闭包?有什么作用什么是闭包闭包是定义在一个函数内部的函数,它可以访问父级函数的内部变量。当一个闭包被创建时,会关联一个作用域—— ... [详细]
  • 深入解析Linux下的I/O多路转接epoll技术
    本文深入解析了Linux下的I/O多路转接epoll技术,介绍了select和poll函数的问题,以及epoll函数的设计和优点。同时讲解了epoll函数的使用方法,包括epoll_create和epoll_ctl两个系统调用。 ... [详细]
  • 计算机存储系统的层次结构及其优势
    本文介绍了计算机存储系统的层次结构,包括高速缓存、主存储器和辅助存储器三个层次。通过分层存储数据可以提高程序的执行效率。计算机存储系统的层次结构将各种不同存储容量、存取速度和价格的存储器有机组合成整体,形成可寻址存储空间比主存储器空间大得多的存储整体。由于辅助存储器容量大、价格低,使得整体存储系统的平均价格降低。同时,高速缓存的存取速度可以和CPU的工作速度相匹配,进一步提高程序执行效率。 ... [详细]
  • 本文介绍了计算机网络的定义和通信流程,包括客户端编译文件、二进制转换、三层路由设备等。同时,还介绍了计算机网络中常用的关键词,如MAC地址和IP地址。 ... [详细]
  • 006_Redis的List数据类型
    1.List类型是一个链表结构的集合,主要功能有push,pop,获取元素等。List类型是一个双端链表的结构,我们可以通过相关操作进行集合的头部或者尾部添加删除元素,List的设 ... [详细]
  • 深入理解Kafka服务端请求队列中请求的处理
    本文深入分析了Kafka服务端请求队列中请求的处理过程,详细介绍了请求的封装和放入请求队列的过程,以及处理请求的线程池的创建和容量设置。通过场景分析、图示说明和源码分析,帮助读者更好地理解Kafka服务端的工作原理。 ... [详细]
  • 基于分布式锁的防止重复请求解决方案
    一、前言关于重复请求,指的是我们服务端接收到很短的时间内的多个相同内容的重复请求。而这样的重复请求如果是幂等的(每次请求的结果都相同,如查 ... [详细]
  • 1、打开etcsysconfiggrub,   #vimetcsysconfiggrub   内容如下: ... [详细]
  • html和js代码互转,html转html5
    本文目录一览:1、html网页跳转javascript代码实现 ... [详细]
author-avatar
王小志2602928087
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有