首页 > 技术文章 > 【状态管理】状态机和状态管理器模型

iCanhua 2019-09-02 00:44 原文

状态

  本文只讨论计算机里面的状态,并且只是讨论对象,对象其实是抽象的产物,所以状态也取决于我们是如何对对象进行抽象和建模的,根据建模方法不同,对象也不同。对象分为有状态的对象和无状态的对象,无状态对象特指那种特性形态固定不变的对象,他们有些在面向对象领域都是单例的,有些是作为值对象存在的,而有状态对象不同,有状态对象通常是多例的。举个例子

  • 有状态对象:Session对象,Session的概念在很多系统中会存在,因为它对每一次会话都会建立该次会话的特征
  • 无状态对象:UserFactory对象,作为一个工厂对象,通常就是无状态的,颜色 Red 对象,一种值对象;

  以前的老系统EJB,有激活和钝化的概念,但现在的编程系统都基本自己负责实现有状态对象的存储了,DDD中有一个概念就是rebuild模式,就是对象的重建。简单的状态是不需要关注的,但是如果要根据状态进行驱动,那么就要对状态进行管理,下面介绍几种状态驱动的模型,第一种是普通的状态,第二种是状态机,第三种是有限Action的状态管理器

普通枚举状态

  这种方式的状态是最简单的对象状态表示,我们时不时都会用上,例如你会在一个实例类中加一个字段:boolean isFinished ;用作表示对象是否已经是完成状态,或者:boolean isRunning;用做表示对象是否在运行状态,这种状态一般都是可枚举完的,而且不像状态模式是状态之间的扭转,而是由调用者设置属性状态,例如 obj.setFinished ;

  注意,你可以不必拘泥于一种方式组织枚举状态,你也可以用一个栈组织这些状态,例如:

/**
 *
 * 代表一个对话管理的目标状态
 *
 **/
private Stack<Goal> interGoal = new Stack();

public void addGoal (Goal newGoal){
    interGoal.push(newGoal);  
}

public void doGoal (){
    Goal currentGoal =  interGoal.peek();
    currentGoal.doGoal;
    interGoal.poll;
}

  如上,对象的状态是对话目标,对话目标可以用栈来表示,这样的好处是:状态可以记录,回退;所以状态的表示,只需要你巧妙利用数据结构,就能达到一些不可思议的效果。

状态模式和策略模式

  两者类图一致;

  策略模式,就是把不同的算法簇一一封装起来,可以被外界对对象进行算法簇替换,这种替换是对象的控制者可以感知的;

  状态模式,以状态为标准,把不同的行为封装到状态类中去,而且处理任何事件都通过状态去处理。如果状态改变,那么处理事件的行为也会被改变,重点是,状态模式的状态改变是对象自身代码自发的,外界不感知;所以控制者看起来就像是对象有自己的意识,发生了很神奇的事情一样。

FMS 有限状态机

  状态机是相对一个系统而言的,其实准确来说,是一个比较核心的对象而言,这个对象的职责和功能,是通过状态机这种数学模型建模得以运作。例如我们的聚合根对象。

  状态机,很多文章介绍会说直接等同于有限状态机(FMS),FSM 解决一个输入序列,经过 FSM,最终停留在什么状态这样一个问题。比较注重的是系统(对象)的状态,和状态之间的转换,状态是历史输入的一种结果,对于一个系统而言,状态机基本包含以下几种模型:

  • 事件(Event)
  • 动作(Action)
  • 状态(State):现态,次态
  • 转换

  状态 State:状态机的状态是有限的,例如一个机器人对话系统的Session对象,就可以抽象出几种状态:

  • 初始状态;
  • 思考决策状态;
  • 动作执行状态;
  • 待用户回复状态;
  • 待服务返回状态;
  • 结束状态。

  而当前时刻,Session只能处于一个状态中;状态转换前后,前一个状态称为现态,后一个状态称为次态

  事件 Event:状态机通常是通过事件驱动运行的,事件代表状态机可以处理的事件,通常发送一件事后,会把事件类型和事件内容一并传递给状态机。状态机接收到事件消息后,调用相应的动作去处理该事件,处理结果可能会是现态到次态的转换

  动作 Action:状态机是如何响应不同的事件的呢,其实每一个状态,都会遇到不同的事件,所以对不同的事件,也是有不同的处理的。比喻在待服务返回状态,发生了待用户说要转换意图的事件,那么事件就是《转换意图》,事件内容是《取消订票》,那么就需要执行 《初始化意图决策流程》这个动作,然后把状态转换为《思考决策状态》。通常事件和状态可以组合为一张表:

事件状态表
属性  初始状态 思考决策状态 动作执行状态 待用户回复状态 待服务返回状态 结束状态
新意图事件 Action1 Action2  ——  —— ——   Action3
服务返回事件  ——  —— Action2  Action1  ——  Action1
用户返回事件 ——  Action1  ——  ——  Action2  ——
服务异常事件  ——  ——  Action3  ——  ——  Action3

 

   状态转换:介绍完动作和事件后,我们就清楚转换是怎么得到的了,也就是状态 s1 接收到事件 event1 后,执行了某些动作 action1,然后还把状态转换为了 s2,下图是机器人Session对象的状态转换图。  

   适用范围:我认为状态机适用于状态对象是具有自我协作意识的场景,状态机才会有意义;它使得不同逻辑的代码之间以多态封装逻辑依赖到各个状态中,从而使得耦合程度最低,易于管理、健壮、维护性好;

  具体为(以下场景摘自spring状态机):

  • 你可以把你的应用或者应用的一部分用状态做领域表示
  • 你需要把复杂的逻辑封装到更小的可以管理的小逻辑
  • 对于并发问题,你需要为此写各种处理异步事件先后到达的繁杂的处理逻辑
  • 如果你已经开始用一些flag或者枚举去做逻辑规划的话,不如把他们当作状态多态化
  • 以上说的flag和变量只是对你的部分逻辑有意义

 

TCP 连接协议状态

  下面给出一个TCP协议的有限状体机的示例,该例子估计大家都看得明白,Client客户端和Server服务端,都对应有自己的状态State和动作Action,分别对应着事件Event和转换

  状态:closed、syn_sent、established、fin_wait1、fin_wait2、time_wait、listen、syn_received、close_wait、last_act

  以上状态区别,一般还按照自身是发起端还是主动关闭端区分,只有close和established两种状态是两个端共有的。

  事件:事件即该端口接收到网络的请求,sys\ack\fin等,这里还有事件附带的数据,例如syn =a ,里面的a就是数据,我们一般称之为 payload

  动作和状态转换关系具体可参考下图:

 

 

状态策略

  状态模式是设计模式的内容,但和状态机不态一样。一个类如果有状态,那么其状态的表示是非常多的,而状态模式很多时候就用一个状态类封装其所处的状态。

  但有时候你需要的系统的状态是很难穷举的。所以状态模式是一种比较低级的应用了。

  无限的状态,有限的Action ——》状态策略:通常,我们的对象建模很难做成状态机,因为状态很可能是无限的,但不管如何,我们的系统执行动作是有限的,也就是State是无限的,而Action是有限的,毕竟Action需要我们程序员手写去实现,那么无限的状态,如果对应这一个个有限的Action呢,我们可以抽象一个根据当前状态选择Action的算法,我们把该算法抽象为<状态策略>注意,这里的策略是Policy,而不是Strategy。在介绍状态策略之前,最好大家先学习一个框架Redux,学习这个是想引出,状态管理器

  Redux:首先介绍下Redux,前端框架的一种,这个自称是状态机实现,每一个组件都是状态机,这个是我认为最合适作为状态机定义的实现,因为React接受外界输入action后,会根据state重新渲染组件,所以会带有观察者模式的感觉,所以我认为它更接近状态机的本质 —— 状态驱动

  下面一段redux的小demo,代表整个redux的工作原理

function createStore(reducer, preloadedState) {
    let state = preloadedState
    let listeners = []

    function subscribe(listener) {
        listeners.push(listener)
    }

    function getState() {
        return state
    }

    function dispatch(action) {
        state = reducer(state, action)
        for (listener of listeners) {
            listener()
        }
    }
    /**通过一个不匹配任何 reducer的 type,来全部的初始值*/
    /**redux 中是使用一个随机的字符串来保证不匹配任何 reducer, 这里使用了 ES6 的 Symbol 类型*/
    dispatch({ type: Symbol() });

    return {
        subscribe,
        getState,
        dispatch
    }
}

  策略:Policy,是一种Action选择的算法实现,它的输入是当前状态,输出是动作 Action,策略可以用规则实现,也可以用机器学习等算法实现。我们现场定义一个策略的接口

public class Policy{
    // 根据当前状态,选择处理这个状态的策略
    Action chooseAction(State state);  

}

  有了接口,我们就不用担心具体的实现,只需要执行Action即可。而Action的执行,当然是交给状态管理器了。随着状态管理器执行完Action,Action会改变状态,也会出发观察者,所有外界就得到通知,外界处理完后会回馈到系统中,系统收到反馈后,这里注意的是,Action会收到这种反馈,然后改变状态State,当State改变后,系统可以把新的State传递给Policy,然后继续这个良性循环,从而整个状态机,得到了驱动。

  状态管理器:Redux就是一个状态管理器,但是比较无奈的是,它接受的输入是外界的Action,使得状态改变,然后通知所有状态的观察者,这种模式的实现,也只有前端比较合适应用了,下面我给大家再介绍一个例子,那就是对话系统的例子,DST(Dialog State Tracker)和DP(Dialog Policy),前者为对话状态跟踪,后者为对话策略,其中对话策略可以用机器学习算法实现;

 

状态机与线程安全

  状态模式,一般情况下,都是一件事一件事发生和处理的,但是有时候我们需要处理的系统并非如此简单,而是大量的事件在不同的事件发生,这个时候,我们可以有以下几种建模方式:

  1、单线程:这种最简单,一个状态机维护一个事件队列,所有事件按照时间的顺序入队,然后用单一的线程处理该队列的事件,这样可以保证每个事件的事务性,但缺点也是有的,处理速度会变得更慢,而且会导致无事件的时候,线程闲置,优点是简单,而且可以给事件作优先级队列,使得重要的事件可以优先处理。

  2、状态锁:可以多线程,但是可以让每个线程在处理的时候获取锁,保证一个时间只有一个线程进行工作;

  3、保持幂等:可以利用事件的处理幂等,一般事件的处理,在需要更新数据库状态的时候,利用乐观锁保证事务的处理是幂等的,如果更新失败,那么就让事件重试,保证了事件是按照逻辑正确处理的;

 

推荐阅读