本文主要是介绍有限状态机TinyFSM使用指南,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
有限状态机TinyFSM使用指南
贺志国
2022-7-14
项目网址:https://github.com/digint/tinyfsm
一、类图
.......
+--------------------------------------: T :
| tinyfsm::FsmList :.....:
+-----------------------------------------|
| [+] set_initial_state() <<static>> |
| [+] reset() <<static>> |
| [+] enter() <<static>> |
| [+] start() <<static>> |
| [+] dispatch(Event) <<static>> |
+-----------------------------------------+.......
+--------------------------------------: T :
| tinyfsm::Fsm :.....:
+-----------------------------------------|
| [+] state<S>() <<static>> |
| [+] set_initial_state() <<static>> |
| [+] reset() <<static>> |
| [+] enter() <<static>> |
| [+] start() <<static>> |
| [+] dispatch(Event) <<static>> |
| [#] transit<S>() |
| [#] transit<S>(Action) |
| [#] transit<S>(Action, Condition) |
+-----------------------------------------+#||+---------------------+| MyFSM |+---------------------+| [+] entry() || [+] exit() || [+] react(EventX) || [+] react(EventY) || ... |+---------------------+#|+-------------+-------------+| | |+---------+ +---------+ +---------+| State_A | | State_B | | ... |+---------+ +---------+ +---------+[#] protected
[+] public
[-] private
二、使用说明
TinyFSM
是一个基于模板撰写的头文件库, 要求编译器支持C++11标准(GCC的编译选项为-std=c++11
,Ubuntu 16.04以上操作系统自带的GCC默认支持)。使用时,只需将头文件tinyfsm.hpp
复制到项目合适的位置,然后使用如下语句包含该头文件即可使用。
#include <tinyfsm.hpp>
TinyFSM
不依赖于RTTI(“Runtime Type Information”的缩写,表示运行时类型信息)、异常或者任何外部库。如果编译时C++标准库(即STL库)都不想使用,请在编译时添加-DTINYFSM_NOSTDLIB
选项,在链接时添加-nostdlib
选项。
下面以Elevator
(电梯)项目为例,对使用TinyFSM
实现状态机进行详细地阐述。Elevator
是一个同时使用两个有限状态机的较复杂示例。
2.1 需求分析
一般而言,一部电梯拥有:
-
“Call”(呼叫)按钮,即每层楼召唤电梯(上升或下降)的按钮;
-
“Floor Sensor”(楼层传感器)按钮,即”1“、”2“、”3“等楼层按钮;
-
“Alarm”(报警)按钮。
说明:当然还有“Open Door(开门)”和“Close Door(关门)”按钮,该项目未提及,大家可以尝试自行添加。
同时,一部电梯包含轿厢(Cabin)和马达(Motor)两大部件。
其中,Cabin的状态包括(后文直接使用Elevator代替Cabin):
- 状态: Idle(空闲)
- 状态: Moving(移动)
- 状态: Panic(恐慌,即故障状态)
Motor的状态包括:
- 状态: Stopped(停止)
- 状态: Up(上升)
- 状态: Down(下降)
特别强调:一个良好的状态机必须避免循环依赖。以Elevator
项目为例:Elevator
可以向Motor
发送事件,但Motor
永远不会向Elevator
发送事件。这样就只存在单向事件传输,否则就是循环依赖。一旦出现循环依赖,系统将变得不可控!
2.2 状态迁移图
-
Elevator
的状态迁移图如下(椭圆表示状态,箭头表示事件):
-
Motor
的状态迁移图如下(椭圆表示状态,箭头表示事件):
2.3 代码分析
2.3.1 声明事件类
声明状态机需要监听的事件类。事件类从tinyfsm::Event
类派生得到,示例代码如下:
struct FloorEvent : tinyfsm::Event{int floor;};struct Call : FloorEvent { };struct FloorSensor : FloorEvent { };struct Alarm : tinyfsm::Event { };
上述代码中,我们定义了三个事件:Call
、FloorSensor
、Alarm
。这三个事件类的对象将会作为实参传给State
类的react()
成员函数。该类中,我们定义了一个成员变量floor
,用于存储Call
和FloorSensors
事件中触发的楼层数字。
2.3.2 声明状态机类
声明状态机类。状态机类从tinyfsm::Fsm<T>
模板类派生得到,其中的T
是状态机自身的类型名。
在状态机类中,需要声明如下公有函数:
- react():为每个事件均声明一个react()函数 ;
- entry() ;
- exit() 。
示例代码如下:
class Elevator: public tinyfsm::Fsm<Elevator>{public:/* default reaction for unhandled events */void react(tinyfsm::Event const &) { };virtual void react(Call const &);virtual void react(FloorSensor const &);void react(Alarm const &);virtual void entry(void) { }; /* entry actions in some states */void exit(void) { }; /* no exit actions */};
注意:为事件声明的react()
函数可以是virtual
,也可以不是virtual
,如果不是virtual
,则不需要查找vtable
(虚函数表),调用效率会更高,但会失去运行时多态的灵活性。
2.3.3 声明状态类
声明状态机的状态类。状态类均从状态机类(即2.3.2节声明的状态机类)派生得到。注意状态类是隐式实例化的。如果大家想在多个状态机中复用状态,需要将其声明为模板类(参考 examples/api/multiple_switch.cpp
)。示例代码如下:
class Panic: public Elevator{void entry() override;};class Moving: public Elevator{void react(FloorSensor const &) override;};class Idle: public Elevator{void entry() override;void react(Call const & e) override;};
该示例中,我们声明了三个状态类:Panic
、Moving
、Idle
。注意在Elevator
示例项目中,并未将这三个类分开声明(即放在不同的头文件中)。因为这几个状态比较简单,过分强调独立性会造成头文件满天飞。当然,如果每个状态内部比较复杂,则需要为其创建一个单独的头文件。
2.3.4 实现动作和对事件的响应
大多数情形下,事件响应函数包含下列一个或多个步骤:
- 改变一些局部数据;
- 给其他状态机发送事件;
- 转移到其他不同的状态。
重要:
在事件响应函数中确保状态迁移动作函数transit<>()
在最后一行调用!
重要:
在entry()/exit()
动作中永远不要调用状态迁移动作函数transit<>()
!
示例代码如下:
void Idle::entry() {send_event(MotorStop());}void Idle::react(Call const & e) {dest_floor = e.floor;if(dest_floor == current_floor)return;/* lambda function used for transition action */auto action = [] { if(dest_floor > current_floor)send_event(MotorUp());else if(dest_floor < current_floor)send_event(MotorDown());};transit<Moving>(action);};
该示例中,我们使用一个Lambda函数作为状态迁移动作函数。transit<>()
函数执行如下动作:
- 调用当前状态的
exit()
函数; - 如果提供了状态迁移动作函数(如本例中的Lambda函数),调用该函数;
- 改变当前状态到新状态;
- 调用新状态的
entry()
函数。
注意,可以将条件函数传递给transit<>()
函数。
2.3.5 定义初始状态
一般情况下,使用宏FSM_INITIAL_STATE(fsm, state)
来定义状态机中的初始状态(或者说是“起始状态”) 。示例代码如下:
FSM_INITIAL_STATE(Elevator, Idle)
上述宏将状态机Elevator
的当前状态设置为Idle
。实际上,在该宏内部为Fsm<Elevator>::set_initial_state()
定义了一个模板特化,并设置当前状态为Idle
。宏FSM_INITIAL_STATE(fsm, state)
的定义如下所示:
#define FSM_INITIAL_STATE(_FSM, _STATE) \
namespace tinyfsm { \template<> void Fsm< _FSM >::set_initial_state(void) { \current_state_ptr = &_state_instance< _STATE >::value; \} \
}
2.3.6 自定义初始化
特定情况下,可能需要自定义初始化,可通过在状态机类中重写(override)reset()
成员函数来达成。如果需要重新初始化所有状态变量,可通过调用
tinyfsm::StateList<MyStates...>::reset()
函数来实现。示例代码如下:
class Switch : public tinyfsm::Fsm<Switch>{public: static void reset(void) {tinyfsm::StateList<Off, On>::reset(); // reset all statesmyvar = 0;...}...}
如果确定重写初始化函数,要确保当前状态已被正确设置,否则在状态机类中会出现对于空指针的解引用。
2.3.7 使用FsmList
分发事件
大家可能已经注意到,在Elevator
项目示例中多次出现对send_event()
函数的调用。注意:该函数不是TinyFSM
提供的。事件分发可通过多种方式实现,TinyFSM
的策略是留给大家自行实现。Elevator
项目借助send_event()
函数来实现直接的事件分发,没有使用事件队列。该方法的优点在于执行速度非常快,因为不需要RTTI(“Runtime Type Information”的缩写,表示运行时类型信息),调用哪个函数来分发事件在编译时就已确定。另一方面,设计状态机时要特别留意避免出现事件循环(即A事件依赖B事件,B事件经过几层转折后又依赖A事件),一旦出现事件循环,证明状态机的设计是完全错误的。
fsmlist.hpp
中的事件分发函数send_event()
:
typedef tinyfsm::FsmList<Motor, Elevator> fsm_list;template<typename E>void send_event(E const & event){fsm_list::template dispatch<E>(event);}
如上所示,send_event()
将事件分发到list
中的所有状态机对象。该方法完成不会带来调用性能的损失,因为在状态机类的声明中,默认的react()
被定义为一个空函数。当然缺点是灵活性有所降低。理解这点很重要。
初始化状态转移日志如下所示:
FsmList<Motor, Elevator>::start()Motor::set_initial_state()Motor::current_state = StoppedElevator::set_initial_state()Elevator::current_state = IdleMotor::enter()Motor:Stopped->entry()cout << "Motor: stopped" <-- HEREMotor::direction = 0Elevator::enter()Elevator:Idle->entry()send_event(MotorStop)Motor::react(MotorStop)Motor:Stopped->transit<Stopped>Motor:Stopped->exit()Motor::current_state = StoppedMotor:Stopped->entry()cout << "Motor: stopped" <-- HEREMotor::direction = 0Elevator::react(MotorStop)
这篇关于有限状态机TinyFSM使用指南的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!