Boost MSM(Meta State Machine)状态机

状态机概念

State machine, state, transition, event

  • State machine:事物的生命周期。它由状态、区域、转换和处理传入事件组成。
    在这里插入图片描述

  • state:状态机生命周期中的一个阶段。一个状态(如子状态机)可以有进入和退出行为。

在这里插入图片描述

  • event:引发(或不引发)状态机反应的事件

  • transition:状态机如何响应事件的规范。它指定了源状态、触发转换的事件、目标状态(如果转换被触发,它将成为新的活动状态)、守卫和动作。

  • Action:在触发转换期间执行的操作。

  • Guard:一种布尔运算,能够阻止原本会触发的转换。

在这里插入图片描述

  • 转换表:状态机的表示。状态机图是一种图形化的,但对同一模型的不完整表示。另一方面,转换表是对同一模型的完整表示。类似以下的播放器的状态表。
struct transition_table : mpl::vector<
//    Start     Event        Target      Action                      Guard 
//   +---------+------------+-----------+---------------------------+--------------------+ 
Row  < Stopped , play       ,  Playing  , start_playback            , none                       >,
Row  < Stopped , open_close ,  Open     , open_drawer               , none                       >,
Row  < Stopped , stop       ,  Stopped  , none                      , none                       >,
//   +---------+------------+-----------+---------------------------+----------------------------+ 
Row  < Open    , open_close ,  Empty    , close_drawer              , none                       >,
//   +---------+------------+-----------+---------------------------+----------------------------+ 
Row  < Empty   , open_close ,  Open     , open_drawer               , none                       >,
Row  < Empty   , cd_detected,  Stopped  , store_cd_info             , good_disk_format           >,
g_row< Empty   , cd_detected,  Playing  , &player_::store_cd_info   , &player_::auto_start       >,
//   +---------+------------+-----------+---------------------------+----------------------------+ 
Row  < Playing , stop       ,  Stopped  , stop_playback             , none                       >,
Row  < Playing , pause      ,  Paused   , pause_playback            , none                       >,
Row  < Playing , open_close ,  Open     , stop_and_open             , none                       >,
//   +---------+------------+-----------+---------------------------+----------------------------+ 
Row  < Paused  , end_pause  ,  Playing  , resume_playback           , none                       >,
Row  < Paused  , stop       ,  Stopped  , stop_playback             , none                       >,
Row  < Paused  , open_close ,  Open     , stop_and_open             , none                       >
//   +---------+------------+-----------+---------------------------+----------------------------+ 
> {};
  • 初始状态:状态机开始时所处的状态。拥有多个正交区域意味着拥有多个初始状态。

在这里插入图片描述

Submachines, orthogonal regions, pseudostates

  • Submachines:子状态机是一个状态机,作为另一个状态机中的一个状态插入,并且可以在同一个状态机中多次找到。

  • orthogonal regions(正交区域):(逻辑)状态机执行的并行流程。状态机的每个区域都有机会处理传入的事件。

在这里插入图片描述

  • **pseudostates(伪状态):**不是一个真实的状态,不持有任何行为或数据,也不能作为状态机的当前状态。它的唯一作用是控制状态转换的流程。

    • 终止伪状态:当此状态变为活动状态时,它会终止整个状态机的执行。然而,MSM 不会像 UML 标准要求的那样销毁状态机,这使您可以保留状态机的所有数据。

    • 进入/退出伪状态:为子状态机定义,并定义为子状态机外部的转换和子状态机内部的转换之间的连接。这是一种通过预定义点进入或离开子状态机的方式。
      在这里插入图片描述

    • 分支:分支允许显式进入子状态机的多个正交区域。
      在这里插入图片描述

    History

    历史是一种记住子状态机活动状态的方式,以便子状态机下次变为活动状态时可以继续其上次的活动状态。

    浅历史:表示子状态机最近子状态的伪状态。一个子状态机最多可以有一个浅历史。以历史伪状态为目标的转换等效于以最近子状态为目标的转换。非常重要的是,只能有一个转换源自历史。

    深历史:递归地重新激活最近子状态的子状态的浅历史。它像浅历史一样表示,但在圆圈内有一个星号 (H*)。
    在这里插入图片描述

Completion transitions / anonymous transitions,Internal transitions,Conflicting transitions

  • Completion transitions / anonymous transitions(完成/匿名转换):当转换没有命名事件触发它时,它会在源状态处于活动状态时自动触发,除非守卫禁止它。
    在这里插入图片描述

  • Conflicting transitions(转换冲突:如果对于给定的源状态和传入事件,存在多种可能的转换,则会发生冲突。UML 规定守卫条件必须解决冲突。
    在这里插入图片描述

  • **Internal transitions(**内部转换):从一个状态到自身状态的转换,而无需调用退出和进入动作。

Meta State Machine(MSM)

MSM设计方式

Boost MSM(Meta State Machine)功能划分为 前端(front‑end) 与 后端(back‑end) 两部分。前端是一种用户界面,具有描述性。后端实现状态机引擎。

  • 前端:负责描述状态机的结构和行为——即状态、事件、转换表以及动作/守卫的写法。Boost MSM 提供了多种前端实现方式。

    1. 基于 MPL(Meta-Programming Library,元编程库) 的前端(使用指向成员函数的指针),

    2. 基于仿函数(functor)的前端,

    3. eUML 前端(基于 Boost.Proto/Boost.Typeof,隐藏大部分元编程细节)

  • 后端:则实现状态机引擎,负责运行时的状态转移、事件分发、入口/退出调用等核心逻辑。目前 Boost MSM 提供两个后端实现。msm::back(boost::msm::back::state_machine), 使用 C++03,msm::back11(boost::msm::back11::state_machine) 使用 C++11。

前端(front-end

Basic front-end (MPL)

  • 示例 带有子状态机的状态机

[CompositeTutorial.cpp]
在这里插入图片描述
状态机的定义:

struct player_ : public msm::front::state_machine_def<player_>{
                            /* see below */}

状态的定义:

struct Empty : public msm::front::state<> 
{
    template <class Event, class Fsm> 
    void on_entry(Event const&, Fsm& ) 
    {std::cout <<"entering: Empty" << std::endl;} 
    template <class Event, class Fsm> 
    void on_exit(Event const&, Fsm& ) 
    {std::cout <<"leaving: Empty" << std::endl;} 
};

初始化状态定义

typedef Empty initial_state;

后端的选择

typedef msm::back11::state_machine<player_> player;

父状态机-转换表

// Transition table for player
struct transition_table : mpl::vector<
    //      Start     Event         Next      Action               Guard
    //    +---------+-------------+---------+---------------------+----------------------+
    a_row < Stopped , play        , Playing , &p::start_playback                         >,
    a_row < Stopped , open_close  , Open    , &p::open_drawer                            >,
    a_row < Stopped , stop        , Stopped , &p::stopped_again                          >,
    //    +---------+-------------+---------+---------------------+----------------------+
    a_row < Open    , open_close  , Empty   , &p::close_drawer                         >,
    //    +---------+-------------+---------+---------------------+----------------------+
    a_row < Empty   , open_close  , Open    , &p::open_drawer                          >,
    a_row < Empty   , cd_detected , Stopped , &p::store_cd_info                        >,
    //    +---------+-------------+---------+---------------------+----------------------+
    a_row < Playing , stop        , Stopped , &p::stop_playback                        >,
    a_row < Playing , pause       , Paused  , &p::pause_playback                       >,
    a_row < Playing , open_close  , Open    , &p::stop_and_open                        >,
    //    +---------+-------------+---------+---------------------+----------------------+
    a_row < Paused  , end_pause   , Playing , &p::resume_playback                      >,
    a_row < Paused  , stop        , Stopped , &p::stop_playback                        >,
    a_row < Paused  , open_close  , Open    , &p::stop_and_open                        >
    //    +---------+-------------+---------+---------------------+----------------------+
> {};

子状态机转换表

/ Transition table for Playing
struct transition_table : mpl::vector4<
    //      Start     Event         Next      Action               Guard
    //    +---------+-------------+---------+---------------------+----------------------+
    a_row < Song1   , NextSong    , Song2   , &pl::start_next_song                       >,
    a_row < Song2   , PreviousSong, Song1   , &pl::start_prev_song                       >,
    a_row < Song2   , NextSong    , Song3   , &pl::start_next_song                       >,
    a_row < Song3   , PreviousSong, Song2   , &pl::start_prev_song                       >
    //    +---------+-------------+---------+---------------------+----------------------+
> {};

后端实例化

player p;
p.start();
// go to Open, call on_exit on Empty, then action, then on_entry on Open
p.process_event(open_close()); pstate(p);
p.process_event(open_close()); pstate(p);
p.process_event(cd_detected("louie, louie"));

Functor front-end

MSM 在MPL前端中提供的行有不同的风格。我们看到了 a_row、g_row、_row、row,还不包括内部行。这些类型有一些缺点

  • 需要更多的输入和信息。

  • 函数指针。

  • 动作/守卫签名受到限制,不允许更多参数的变化(源状态、目标状态、当前状态机等)。

  • 从一个状态机到另一个状态机重用动作代码并不容易。

[SimpleWithFunctors.cpp]

转换表

转换现在是 “Row” 类型,正好有 5 个模板参数:源状态、事件、目标状态、动作和守卫。在没有任何内容的地方(例如动作和守卫),写入 “none”。动作和守卫不再是方法,而是functor,获取检测到的事件、状态机、源状态和目标状态作为参数。

struct transition_table : mpl::vector<
    //    Start     Event         Next      Action                     Guard
    //  +---------+-------------+---------+---------------------------+----------------------+
    Row < Stopped , play        , Playing , ActionSequence_
                                             <mpl::vector<
                                             TestFct,start_playback> >
                                                                      , DummyGuard           >,
    Row < Stopped , open_close  , Open    , open_drawer               , none                 >,
    Row < Stopped , stop        , Stopped , none                      , none                 >,
    //  +---------+-------------+---------+---------------------------+----------------------+
    Row < Open    , open_close  , Empty   , close_drawer              , none                 >,
    //  +---------+-------------+---------+---------------------------+----------------------+
    Row < Empty   , open_close  , Open    , open_drawer               , none                 >,
    Row < Empty   , cd_detected , Stopped , store_cd_info             , And_<good_disk_format,
                                                                             always_true>    >,
    // we here also mix with some "classical row"
  g_row < Empty   , cd_detected , Playing                             , &p::auto_start       >,
    //  +---------+-------------+---------+---------------------------+----------------------+
    Row < Playing , stop        , Stopped , stop_playback             , none                 >,
    Row < Playing , pause       , Paused  , pause_playback            , none                 >,
    Row < Playing , open_close  , Open    , stop_and_open             , none                 >,
    //  +---------+-------------+---------+---------------------------+----------------------+
    Row < Paused  , end_pause   , Playing , resume_playback           , none                 >,
    Row < Paused  , stop        , Stopped , stop_playback             , none                 >,
    Row < Paused  , open_close  , Open    , stop_and_open             , none                 >
    //  +---------+-------------+---------+---------------------------+----------------------+
> {};

Actions的写法

struct start_playback
{
    template <class EVT,class FSM,class SourceState,class TargetState>
    void operator()(EVT const& ,FSM& ,SourceState& ,TargetState& )
    {
        cout << "player::start_playback" << endl;
    }
};

eUML

eUML 不再继续开发。不建议使用了。

基于 Boost.Proto 和 Boost.Typeof 的编译时特定领域嵌入式语言。它提供了语法,允许直接在转换表或状态定义中的 entry/exit 中定义动作/守卫。有用于动作、守卫、标志、属性、延迟事件、初始状态的语法。

BOOST_MSM_EUML_DECLARE_TRANSITION_TABLE((
          Stopped + play        / start_playback                    == Playing               ,
          Stopped + open_close  / open_drawer                       == Open                  ,
          Stopped + stop                                            == Stopped               ,
          //  +------------------------------------------------------------------------------+
          Open    + open_close  / close_drawer                      == Empty                 ,
          //  +------------------------------------------------------------------------------+
          Empty   + open_close  / open_drawer                       == Open                  ,
          Empty   + cd_detected /(store_cd_info,
                                  msm::front::euml::process_(play)) == Stopped               ,
          //  +------------------------------------------------------------------------------+
          Playing + stop        / stop_playback                     == Stopped               ,
          Playing + pause       / pause_playback                    == Paused                ,
          Playing + open_close  / stop_and_open                     == Open                  ,
          //  +------------------------------------------------------------------------------+
          Paused  + end_pause   / resume_playback                   == Playing               ,
          Paused  + stop        / stop_playback                     == Stopped               ,
          Paused  + open_close  / stop_and_open                     == Open     
          //  +------------------------------------------------------------------------------+
          ),transition_table)

三种实现方式比较

Functor前端是目前首选的前端。它比MPL前端更强大,并且具有更易读的转换表。它还使重用状态机部分变得更容易。

MPL前端 Functor前端 eUML前端
特点
基于模板元编程(MPL),提供了高度的灵活性和控制力,但语法复杂。 基于函数对象(Functors),简化了状态机的定义和实现,提高了代码的可读性和可维护性 不考虑

后端(Back-end)

目前有两种后端。msm::back 使用 C++03,msm::back11 使用 C++11,如果可能,应使用C++11,因为它消除了 C++11 之前的一些限制(mpl::vector 的大小有限)。

创建

MSM 在前端和后端之间划分,首先需要定义一个前端。然后,要创建一个真正的状态机,必须声明后端。

typedef msm::back11::state_machine<my_front_end> my_fsm;

启动和停止状态机

start() 方法启动状态机,这意味着它将激活初始状态。

stop() 方法的工作方式相同。它将导致调用当前活动状态的退出动作。

事件分发(Event dispatching)

my_fsm fsm; fsm.process_event(some_event());
some_event e1; fsm.process_event(e1)

活动状态(Active state(s))

const int* current_state() 

上层状态机

后端提供了一个 get_upper() 函数,返回指向上层状态机的指针。

序列化(Serialization)

常见的需求是能够保存状态机并在不同的时间恢复它。对于基本序列化,MSM 状态机几乎像任何其他类型一样可序列化。无需任何特殊工作,可以使状态机记住其状态。

[Serialize.cpp]

获取状态

访问状态的数据

player::Stopped* tempstate = p.get_state<player::Stopped*>();

将事件排队以供稍后处理

调用 process_event(Event const&) 将立即使用运行到完成语义处理事件。如果想将事件排队并延迟其处理,方法是调用 enqueue_event(Event const&) 代替。然后,调用 execute_queued_events() 将处理所有排队的事件(按 FIFO 顺序)。调用 execute_single_queued_event() 将执行最旧的排队事件。

通过调用 get_message_queue_size() 查询队列大小。

其它

  • 标志。标志是 MSM 独有的概念,所有前端都支持它,它们基于以下函数。如果当前活动状态支持 Flag 属性,这些函数将返回 true。
template <class Flag> bool is_flag_active()
template <class Flag,class BinaryOp> bool is_flag_active()
  • 带参数的状态机构造函数

定义一个带有非默认构造函数的状态机,使用后端作为转发对象。

struct player_ : public msm::front::state_machine_def<player_> 
  {
      player_(int some_value){…} 
  }; 
typedef msm::back11::state_machine<player_ > player; player p(3);

示例

正交区域、终止状态、事件延迟

在许多状态机中,必须处理错误是一个非常常见的问题。它通常涉及定义从所有状态到特殊错误状态的转换。

UML 提供了一个有用的概念,正交区域。将它们视为在公共状态机内部同时运行并具有相互影响能力的轻量级状态机。效果是在任何时候都有多个活动状态。

当检测到错误时,通常不希望进一步处理事件。为了实现这一点,使用另一个 UML 功能,终止状态。当任何区域移动到终止状态时,状态机“终止”(状态机及其所有状态都保持活动状态),并且所有事件都被忽略。

假设有人放入光盘并立即按下播放。该事件无法处理,但您希望稍后处理它,而不是强迫用户再次按播放。解决方案是在 Empty 和 Open 状态中将其定义为延迟,并在第一个事件不被延迟的状态中处理它。然后可以处理或拒绝它。在此示例中,当 Stopped 变为活动状态时,事件将被处理,因为只有 Empty 和 Open 延迟了该事件。
在这里插入图片描述

[Orthogonal-deferred2.cpp]

历史

如果播放器正在播放第二首歌,并且用户按下了暂停,离开了 Playing 状态,则在下次按下播放按钮时,Playing 状态将变为活动状态,并且将再次播放第一首歌。很快就会收到第一批客户投诉。他们当然会要求,如果播放器已暂停,则应记住正在播放哪首歌。但是如果播放器已停止,则应从第一首歌重新开始。
在这里插入图片描述

[History.cpp]

显式进入/进入和退出伪状态/分支

如下图所示

  • 由 event1 触发的 SubFsm2 的“正常”激活。在每个区域中,初始状态被激活,即 SubState1 和 SubState1b。

  • 区域“1”显式进入 SubFsm2::SubState2,以 event2 作为触发器,这意味着在区域“2”中,初始状态 SubState1b 被激活。

  • 分支到区域“1”和“2”,显式进入 SubState2 和 SubState2b,由 event3 触发。两个状态都变为活动状态,因此没有区域默认激活(如果我们有第三个区域,则会)。

  • 通过进入伪状态 SubFsm2::PseudoEntry1 连接两个转换,由 event4 触发,并且还触发同一事件的第二个转换(两个转换都必须由同一事件触发)。区域“2”是默认激活的,SubState1b 变为活动状态。

  • 使用退出伪状态 PseudoExit1 从 SubFsm2 退出,由 event5 触发,并使用同一事件连接两个转换。同样,该事件被转发到第二个转换,并且两个区域都退出,因为 SubFsm2 变为非活动状态。请注意,如果未从 PseudoExit1 定义转换,则将检测到错误(如 UML 标准中定义),并调用 no_transition。
    在这里插入图片描述

[DirectEntryTutorial.cpp]

标志

在这里插入图片描述

[Flags.cpp]

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐