今天看到boost有个库叫做statechart. 不禁兴趣较大,学习了一下,受益匪浅.
大体来说,这个库处理了大部分状态机uml中涉及到的点.
1. 简单状态处理
如上图,我们暂时认为acive是一个简单状态而不是一个复合状态. 那么按照状态机来说,有几个元素呢?
1) 初始状态
2) 转换事件/action
3) 中间状态
4) 结束状态 (暂无)
那么boost::statechart 重现过后,有如下对应对象
1) state_machine
2) simple_state / state
3) event
4) transition / reaction
我们来看看大致的代码对应:
1. 定义一个状态机对象
struct Machine : sc::state_machine
{
Machine()
{
std::cout << __FUNCTION__ << std::endl;
}
~Machine()
{
std::cout << __FUNCTION__ << std::endl;
}
};
2. 定义初始状态对象
struct StatActive : sc::simple_state
{
typedef sc::transition reactions;
StatActive()
{
m_lElapsed = 0;
std::cout << __FUNCTION__ << std::endl;
}
~StatActive()
{
std::cout << __FUNCTION__ << std::endl;
}
};
3. 定义转化,注意第二步中已经贴出转换步骤代码,即typedef 部分
4. 启动状态机
Machine machine;
machine.initiate();
machine.process_event(EvReset());
machine.terminate();
就这样一个简单的状态机以及其相关的逻辑均已经体现.
下面我们来看一个复杂点的状态机,一个数码相机的例子;
先来分析一下这个状态机有哪些元素:
1. 有3个状态
1) notshooting (复合)
2) shooting (复合)
3) storing (基本)
所谓基本和复合大致的区别是符合状态内部还有小的状态机.
2. 有如下几种动作转换
1) HalfPress
a) 自动对焦模式
b) 手动对焦模式
2) FullPress
a) 手动对焦模式
3) Release
下面再来看复合状态
notshooting 内部分为:
1. idle状态
2. 配置状态
有一个转换事件:
1. config
shooting分为:
1. focusing
2. focused
3. storing
事件:
1. infocus
2. fullpressed with condition
具体代码就不贴了,可以参见 d:\boost\test\state_machine
特别说明:
看到了focusing这个状态了吧, 一般来说这个状态是需要持续一段时间的, 所以如果这段时间内用户按下了fullpress,如果按照上面的状态机处理,则此状态将会被丢弃; 这儿如果我们做一个改进,即将此状态记录下来,然后当用户再次对焦准确后自动触发,这就是boost::derreral的作用.
typedef mpl::list<
sc::transition< EvInfocus, StatFocused >,
// sc::transition< EvFullPress , StatFocusing>
sc::deferral
> reactions;
但是据我测试,如果多个fullpress的话,只会有一个生效。
一个小技法:
大部分情况下如果
typedef sc::transition reactions;
那么往往当状态发生转移时候,目标状态的对象会被构造;即通过构造函数来呼叫;
但是也可通过
typedef sc::transition< EvInFocus, Focused,
Shooting, &Shooting::DisplayFocused > reactions;
这种方式,来指定对target的呼叫,甚至可以看到这个target的处理函数还不是focused对象。
从跟踪来看,此函数的call 遭遇focused 状态的构造。
事件通知机制
目前看到的transit 方式实际上优点类似于一种同步方式;
但是实际上还可以通过另外一种方式处理,即状态切换推迟到当前的reaction结束;
比如:
StatFocused::~StatFocused()
{
post_event( EvPumpingFinished() );
}
那么post_event干了些啥呢?
它将一个事件放入主队列,状态机将在当前reaction处理结束后立刻处理此事件;
事件可以在reaction , enter entry , exit entry , transaction reaction 中被触发,但是在
内部的入口和出口点时候有一点复杂。
struct Pumping : sc::state< Pumping, Purifier >
{
Pumping( my_context ctx ) : my_base( ctx )
{
post_event( EvPumpingStarted() );
}
// ...
};
即需要发送的状态,必需派生自state , 且实现入口点的构造,如上.
只要一个入口点的动作需要和外部世界解除,比如主队列,那么就需要如此,类似的函数有:
simple_state<>::post_event()
simple_state<>::clear_shallow_history<>()
simple_state<>::clear_deep_history<>()
simple_state<>::outermost_context()
simple_state<>::context<>()
simple_state<>::state_cast<>()
simple_state<>::state_downcast<>()
simple_state<>::state_begin()
simple_state<>::state_end()
但是幸运的是一般我们总是在react或者transaction中处理,因此一般来说不需要考虑。
这儿来举个例子,比如focusing 状态到 focused 状态,实际上后一个状态由前一个状态触发,而且从实际操作来看,是很有可能在前者的entry action中完成了对焦,然后触发此事件, 因此这个可能就是一个内部动作。
我之前的例子是,类似如下方式测试的:
std::cout << "testing focusing fullpressed" << std::endl;
machine.MemAvaiable() = true;
machine.AutoFocus() = true;
machine.process_event(EvHalfPress());
machine.process_event(EvInfocus());
machine.process_event(EvFullPress());
machine.process_event(EvRelease());
即是主动发送EvInfocus ,这个就是说外部出发了focusing 后又触发infocus,实在点说,这个可能性不如内部处理来得大。
看看输出:
再来看修改过后的代码:
std::cout << "testing focusing fullpressed" << std::endl;
machine.MemAvaiable() = true;
machine.AutoFocus() = true;
machine.process_event(EvHalfPress());
//machine.process_event(EvInfocus());
machine.process_event(EvFullPress());
machine.process_event(EvRelease());
struct StatFocusing : sc::state
{
typedef sc::transition< EvInfocus, StatFocused,
StatShooting, &StatShooting::DisplayFocuesed > reactions;
/*
typedef mpl::list<
sc::transition< EvInfocus, StatFocused >,
// sc::transition< EvFullPress , StatFocusing>
sc::deferral
> reactions;
*/
StatFocusing( my_context ctx ) : my_base( ctx )
{
std::cout << "enter focusing" << std::endl;
post_event(EvInfocus());
}
~StatFocusing()
{
std::cout << "leave focusing" << std::endl;
}
};
从输出来看是一致的。
不过过程是不一致的,后者是在machine.process_event(EvHalfPress())之后
machine.process_event(EvFullPress())之前得到处理的.
历史记录的处理:
由于idle状态是个复合状态,即其有两个状态,idle 和 config;那么也就意味着用户是可以在这两个状态的任何一个,按动 half-press , full-press 进行操作后,将返回到idle状态;但是有些情况下,用户也许希望回到前一个状态,比如曾经是在配置界面进入拍摄的。
请观察图中shooting 到notshooting 直接的H*,这这个标示状态切换前需要回到历史状态,*标示状态为0或者多个。
std::cout << "testing history" << std::endl;
machine.process_event(EvConfig());
machine.process_event(EvHalfPress());
machine.process_event(EvFullPress());
machine.process_event(EvRelease());
从上图可以看到,从拍照结束后进入到了config状态,即事发前的状态.
到了这儿,其实还有一个问题,即deep_history , 但是boost还提供了一个shallow_history,
从字面来看;另外从boost的表述,deep_history实际上是保存了一个内部状态的hierachy,然后返回时候返回到最内层状态,我们继续一下测试;修改config的状态图如下:
上图我们将config的状态从简单状态修改为一个复合状态,即其内部含有一个历史图片配置,以及单张图片配置两个内部状态。
从上图看到,在返回历史记录时候层次关系式ok的。
接下来看看shallow 和deep的差别;
std::cout << "testing history" << std::endl;
machine.process_event(EvConfig());
machine.process_event(EvSingleConfig());
machine.process_event(EvHalfPress());
machine.process_event(EvFullPress());
machine.process_event(EvRelease());
如果设定为has_shallow_hisotry shallow_hisotory 那么输出很上面相似;
如果启用has_deep_hisotory deep_history 那么输出如下:
你会发现,deep的效果能够追述到内部状态的切换;
另外还存在一个has_full_history deep_history,boost的文档说其相当于 shallow & deep.
更加的具体的我从测试中未发现差异。或许唯一的差别就是,当外界有时候会以 deep_history 有时候会以shallow_history 定义历史callback时候,has_full_history 就会有随之发生变化;
为了更加清晰的展示一个实用的例子, 再来看看修改后的uml图:
即开机后进入notshooting状态,但是notshooting状态也带有上次关机前的历史记录.
特别说明,由于visio的uml图无法画出inside inner state的图,因此H+只能挑个合适的地方画,比如shooting到idle的转换间的H+,被移动到了这儿,严格来说这个H+确实应该在状态之内,即notshooting之内.
std::cout << "testing idle hisotry" << std::endl;
machine.process_event(EvPowerOn());
machine.process_event(EvConfig());
machine.process_event(EvSingleConfig());
machine.process_event(EvPowerOff());
machine.process_event(EvPowerOn());
还有一些比较正交的状态图:
或者异常处理时候的的处理,这儿不再列举,有兴趣参考
。