有限状态机可能是编程中最基本,使用最广泛的概念之一。 有限状态机(KA)积极地应用于许多应用领域。 特别是,在可以处理的诸如APCS和电信这样的利基市场中,发现航天器的频率要比每一步都要少。
因此,在本文中,我们将尝试讨论航天器,主要是关于分层有限状态机及其高级功能。 并介绍一下
SObjectizer-5中对航天器的支持,
SObjectizer-5是C ++的“角色”框架。 开放,免费,跨平台的,仍然活跃的那
两个 。
即使您对SObjectizer并不感兴趣,但您从未听说过分层有限状态机,或者从未听说过航天器的这种高级功能(例如用于状态或状态历史记录的输入/输出处理程序)的实用性,那么您可能还是有兴趣在猫下找东西,至少阅读本文的第一部分。
关于有限状态机的一般词汇
我们不会在有关
自动机和诸如
有限状态机之类的文章中尝试进行完整的教育计划。 读者至少需要对这些类型的实体有基本的了解。
先进的有限状态机及其功能
该航天器具有几个“高级”功能,可大大提高航天器在程序中的可用性。 让我们快速浏览一下这些“高级”功能。
免责声明:如果读者熟悉UML的状态图,那么他将不会在这里找到任何新东西。
分层状态机
也许最重要和最有价值的机会是组织层次结构/嵌套状态。 由于正是由于将状态彼此转换的能力,才消除了随着航天器复杂性的增加,从一个状态到另一个状态的转换数量的“爆炸式增长”。
用言语来解释这一点比通过例子来说明要困难得多。 因此,假设我们在屏幕上有一个信息亭,首先显示欢迎消息。 用户可以选择“服务”项,然后转到选择所需服务的部分。 或者,他可以选择“个人帐户”项目,然后转到有关使用其个人数据和服务的部分。 或者,他可以选择“帮助”部分。 到目前为止,一切似乎都很简单,可以用以下状态图表示(尽可能简化):

但是,让我们尝试确保通过单击“取消”按钮,用户可以通过欢迎消息从任何部分返回到起始页面:

该方案越来越复杂,但仍处于控制之中。 但是,让我们回想一下,在“服务”部分中,我们可能还有几个小节,例如,“热门服务”,“新服务”和“完整列表”。 从每个部分中,您还需要返回到起始页面。 我们简单的航天器变得越来越困难:

但这远非全部。 我们尚未考虑“返回”按钮,需要通过该按钮返回上一部分。 让我们在“返回”按钮上添加一个反应,看看会得到什么:

是的,现在我们看到了通往真正乐趣的道路。 但是我们甚至都没有考虑过“我的帐户”和“帮助”部分中的小节……如果开始,那么几乎一开始我们的简单航天器几乎会变成难以想象的事情。
在这里,国家的嵌套对我们有帮助。 假设我们只有两个顶级状态:WelcomeScreen和UserSelection。 我们所有的部分(即“服务”,“我的帐户”和“帮助”)都将处于“用户选择”状态的“嵌套”状态。 您可以说ServicesScreen,ProfileScreen和HelpScreen状态将是UserSelection的子级。 由于他们是孩子,他们将从父母的状态继承对某些信号的反应。 因此,我们可以在UserSelection中定义对“取消”按钮的响应。 但是,我们无需在所有子州中都确定这种反应。 是什么让我们的航天器更简洁易懂:

在这里,您可以注意到我们在UserSelection中定义的对“取消”和“返回”的反应。 而且,对“取消”按钮的这种反应适用于所有的UserSelection子状态(包括另一个复合ServicesSelection子状态)。 但是在ServicesSelection子状态中,对“后退”按钮的反应已经不同了-返回值不在WelcomScreen中,而在ServicesScreen中。
使用状态的层次结构/嵌套的CA被称为层次结构有限状态机(ICA)。
对进入/退出状态的反应
一个非常有用的功能是能够分配对进入特定状态的响应以及对退出状态的反应的能力。 因此,在上述带有信息亭的示例中,可以挂起处理程序以输入每个状态,这将更改信息亭屏幕的内容。
前面的示例可以扩展一点。 假设我们在WelcomScreen中有两个子状态:BrightWelcomScreen(屏幕将正常高亮显示)和DarkWelcomScreen(屏幕亮度将降低)。 我们可以使DarkWelcomScreen条目处理程序使屏幕变暗。 还有一个DarkWelcomScreen退出处理程序,它将恢复正常亮度。

设定时间后自动更改状态
有时,可能有必要限制航天器停留在特定状态。 因此,在上面的示例中,我们可以将ICA保持在BrightWelcomScreen状态的时间限制为一分钟。 分钟一结束,ICA就会自动切换到DarkWelcomScreen状态。
航天器历史
ICA的另一个非常有用的功能是航天器状态的历史。
假设我们有这种抽象的ICA:

我们的ICA可以从TopLevelState1到TopLevelState2,反之亦然。 但是在TopLevelState1内部有几个嵌套状态。 如果ICA仅从TopLevelState2移到TopLevelState1,则立即激活两个状态:TopLevelState1和NestedState1。 NestedState1被激活,因为它是TopLevelState1状态的初始子状态。
现在想象一下,我们的ICA进一步将其状态从NestedState1更改为NestedState2。 在NestedState2内部,子状态InternalState1被激活(因为它是NestedState2的初始子状态)。 然后从InternalState1转到InternalState2。 因此,我们同时具有以下活动状态:TopLevelState1,NestedState2和InternalState2。 在这里,我们转到TopLevelState2(即,我们通常离开TopLevelState1)。
活动变为TopLevelState2。 之后,我们要返回TopLevelState1。 它位于TopLevelState1中,而不位于TopLevelState1中的任何特定子状态中。
那么,从TopLevelState2转到TopLevelState1,该去哪里?
如果TopLevelState1没有历史记录,那么我们将进入TopLevelState1和NestedState1(因为NestedState1是TopLevelState1的初始子状态)。 即 在离开TopLevelState2之前发生的有关TopLevelState1内部过渡的整个故事都完全丢失了。
如果TopLevelState1有一个所谓的 浅层历史记录,然后从TopLevelState2返回TopLevelState1时,我们进入NestedState2和InternalState1。 我们进入NestedState2,因为它记录在TopLevelState1的状态历史记录中。 我们进入InternalState1,因为它是NestedState2的起始位置。 事实证明,在TopLevelState1的肤浅历史中,仅存储有关第一级子状态的信息。 这些子状态中嵌入状态的历史记录未保留。
但是,如果TopLevelState1历史悠久,那么当我们从TopLevelState2返回TopLevelState1时,便会进入NestedState2和InternalState2。 因为在较深的历史记录中,将存储有关活动子状态的完整信息,而不管其深度如何。
正交态
到目前为止,我们已经检查了ICA,其中只有一个子状态可以在该状态内处于活动状态。 但是有时可能会出现在ICA的特定状态下应该同时存在多个活动子状态的情况。 这种子状态称为正交状态。
演示正交状态的经典示例是熟悉的计算机键盘及其NumLock,CapsLock和ScrollLock模式。 我们可以说使用NumLock / CapsLock / ScrollLock是通过Active状态内的正交子状态来描述的:

您想了解的有关有限状态机的所有信息,但是...
通常,David Harel撰写了一篇有关状态图形式表示法的基础文章:
Statecharts:复杂系统的可视形式主义(1987) 。
在那里,以控制普通电子时钟为例,研究了在使用有限状态机时可能遇到的各种情况。 如果有人没有读过,我强烈推荐。 基本上,Harel随后描述的所有内容都变为UML表示法。 但是,当您从UML阅读状态图的描述时,您并不总是了解需要什么,原因和时间。 但是在Harel的文章中,演示从简单的情况到更复杂的情况。 而且您可以更好地了解有限状态机自身所具有的全部功能。
SObjectizer中的有限状态机
此外,我们将讨论SObjectizer及其细节。 如果您不太了解以下示例,那么可能有必要进一步了解SObjectizer。 例如,从我们
有关SObjecizer的
评论文章以及随后的几篇
文章中向读者介绍SObjectizer,从简单到复杂(
第一篇,
第二篇和
第三篇 )。
SObjectizer中的代理是状态机
从一开始,SObjectizer中的代理就是具有显式状态的状态机。 即使代理的开发人员未在其代理类中描述其自己的任何状态,该代理仍然具有默认状态,该状态默认使用。 例如,如果开发人员制作了这样的琐碎代理:
class simple_demo final : public so_5::agent_t { public:
那么他甚至可能不会怀疑实际上他所做的所有订阅都是针对默认状态进行的。 但是,如果开发人员将自己的状态添加到代理中,那么您已经必须考虑以正确的状态正确签署代理。 举例来说,这里是对上面显示的代理的简单(和往常一样)错误的修改:
class simple_demo final : public so_5::agent_t {
我们为how_are_you信号设置了两个不同的处理程序,每个处理程序都有其自己的状态。
而且此simple_demo代理程序修改中的错误是,处于st_free或st_busy时,该代理程序完全不会响应退出,因为 我们将退出订阅保留为默认状态,但没有为st_free和st_busy进行相应的订阅。 解决此问题的一种简单而明显的方法是将适当的订阅添加到st_free和st_busy中:
simple_demo(context_t ctx) : so_5::agent_t{std::move(ctx)} {
没错,这种方法带有复制粘贴的痕迹,这不好。 您可以通过为st_free和st_busy输入公共父状态来摆脱复制粘贴:
class simple_demo final : public so_5::agent_t {
为了公正起见,应该补充一点,最初在SObjectizer代理中只能是简单的状态机。 相对较新的支持出现在2016年1月。
为什么SObjectizer代理是有限状态机?
这个问题有一个非常简单的答案:
碰巧 SObjectizer
的根源来自过程控制系统领域,在那儿经常使用有限状态机。 因此,我们认为SObjectizer中的代理也必须是状态机。 如果在他们要为其应用SObjectizer的应用程序中使用CA,这将非常方便。 并且所有代理都具有默认状态,如果不需要使用CA,则无需考虑CA。
原则上,如果您查看Actors模型本身以及构建此模型的原则,则:
- 演员是具有行为的实体;
- 演员响应传入的消息;
- 收到消息后,演员可以:
- 向其他演员发送一定数量的消息;
- 创建一些新的演员;
- 定义用于处理后续消息的新行为。
人们可以在简单的航天器和演员之间找到强烈的相似之处。 您甚至可以说参与者是简单的有限状态机。
SObjectizer支持哪些高级状态机功能?
在高级有限状态机的上述功能中,SObjectizer支持除正交状态之外的所有功能。 支持其他优点,例如嵌套状态,输入/输出处理程序,对在该状态上花费的时间的限制,状态的历史记录。
在正交状态的支持下,第一次并没有一起发展。 一方面,SObjectizer的内部体系结构不旨在支持代理的几个独立且同时处于活动状态。 另一方面,关于具有正交状态的主体应如何行为,存在意识形态问题。 这些问题的纠结太复杂了,有用的排气量也太小,无法解决这个问题。 是的,在我们的实践中,还没有需要正交状态的情况,但是不可能做到这一点,例如,通过将工作划分到与一个共同的工作环境相关联的多个主体之间。
但是,如果有人需要诸如正交状态之类的特征,并且您有一些实际的任务示例,那么就让我们开始吧。 也许在我们眼前有具体的例子,我们可以将此功能添加到SObjectizer中。
在代码中如何支持ICA的高级功能
在故事的这一部分中,我们将尝试快速介绍SObjectizer-5 API以与ICA一起使用。 在不深入细节的情况下,只是让读者了解什么是外观。 如果您愿意,可以
在官方文档中找到更多详细信息。
嵌套国家
为了声明一个嵌套状态,您需要将initial_substate_of或substate_of表达式传递给相应的state_t对象的构造函数:
class demo : public so_5::agent_t { state_t st_parent{this};
如果状态S具有多个子状态C1,C2,...,Cn,则应将其中一个(只有一个)标记为initial_substate_of。 在运行时诊断是否违反此规则。
SObjectizer-5中状态嵌套的最大深度受到限制。 在5.5版中,这些是16个级别。 在运行时诊断是否违反此规则。
嵌套状态最重要的窍门是,当激活具有嵌套状态的状态时,会一次激活多个状态。 假设状态A具有子状态B和C,在子状态B中具有子状态D和E:

激活状态A后,实际上,立即激活了三个状态:A,AB和ABD
几个州可以同时处于活动状态这一事实对两个档案事物具有最严重的影响。 首先,搜索下一条传入消息的处理程序。 因此,在刚刚显示的示例中,将首先在ABD状态下搜索消息处理程序,如果那里没有合适的处理程序,则搜索将以其父状态继续进行,即 在AB中并且已经受到伤害,如有必要,搜索将在状态A中继续。
其次,几个活动状态的存在会影响状态的输入/输出处理程序的调用顺序。 但这将在下面讨论。
状态I / O处理程序
对于状态,可以指定状态进入和退出状态处理程序。 这是使用state_t :: on_enter和state_t :: on_exit方法完成的。 通常,这些方法在so_define_agent()方法中调用(或者,如果代理是琐碎的并且没有提供从其继承的信息,则直接在代理构造函数中调用)。
class demo : public so_5::agent_t { state_t st_free{this}; state_t st_busy{this}; ... void so_define_agent() override {
使用on_enter / on_exit处理程序时,最困难的时刻可能是将它们用于嵌套状态。 让我们回到状态A,B,C,D和E的示例。

假设每个状态都有一个on_enter和on_exit处理程序。
让A.成为代理的当前状态。 状态A,AB和ABD被激活在代理程序状态更改期间,将调用A.on_enter,ABon_enter和ABDon_enter。 并以此顺序。
假设有一个到ABE的转换,将调用ABDon_exit和ABEon_enter。
如果然后将代理置于AC状态,则将调用ABEon_exit,ABon_exit,ACon_enter。
如果处于AC状态的代理被注销,则so_evt_finish()方法完成后,将立即调用ACon_exit和A.on_exit处理程序。
时间限制
使用state_t :: time_limit方法设置代理保持在特定状态的时间限制。 与on_enter / on_exit一样,通常在将代理配置为在SObjectizer内部工作的情况下调用time_limit方法:
class led_indicator : public so_5::agent_t { state_t inactive{this}; state_t active{this}; ... void so_define_agent() override {
如果设置了该状态的时间限制,则代理程序进入此状态后,SObjectizer就会开始计算在该状态中花费的时间。 如果代理离开状态,然后再次返回到该状态,则倒数再次开始。
如果为嵌入式状态设置了时间限制,则需要注意,因为 可能有奇怪的把戏:
class demo : public so_5::agent_t {
假设代理进入状态A. A和C都激活了状态A和C。 以前,它将在状态C结束,并且代理将切换到状态D。这将开始倒计时,以保持在状态D。但是倒计时将继续保持在A! 由于在从C到D的过渡期间,代理继续保持状态A。在从C到D的强制过渡之后的五秒钟,代理将进入状态B。
财富故事
默认情况下,座席状态没有历史记录。 要激活状态的历史记录保存,请将shallow_history常量(状态将具有较浅的历史记录)或deep_history(状态将具有较深的历史记录)传递给state_t构造函数。 例如:
class demo : public so_5::agent_t { state_t A{this, shallow_history}; state_t B{this, deep_history}; ... };
状态的历史是一个困难的话题,尤其是当使用了适当深度的状态嵌套并且子状态具有自己的历史时。 因此,有关此主题的更多完整信息,最好参考
文档和实验。 好吧,问我们您是否无法自行解决;)
just_switch_to,transfer_to_state,禁止
state_t类具有许多最常用的方法,这些方法已在上面显示:event()用于将事件预订到消息,on_enter()和on_exit()用于设置输入/输出处理程序,time_limit()用于设置状态花费的时间限制。
与这些方法一起使用ICA时,state_t类的以下方法非常有用:
方法just_switch_to(),该方法专门用于当传入消息的唯一反应是将代理转移到新状态时的情况。 您可以写:
some_state.just_switch_to<some_msg>(another_state);
代替:
some_state.event([this](mhood_t<some_msg>) { this >>= another_state; });
当我们在两个或多个状态S1,S2,...,Sn中以相同的方式处理某些消息M时,transfer_to_state()方法非常有用。 但是,如果我们处于状态S2,...,Sn,那么我们首先必须返回到S1,然后才进行处理M。
如果这听起来很棘手,那么也许可以在代码示例中更好地理解这种情况:
class demo : public so_5::agent_t { state_t S1{this}, S2{this}, ..., Sn{this}; ... void actual_M_handler(mhood_t<M> cmd) {...} ... void so_define_agent() override { S1.event(&demo::actual_M_handler); ...
但是,不要为S2,...,Sn定义非常类似的事件处理程序,请使用transfer_to_state:
class demo : public so_5::agent_t { state_t S1{this}, S2{this}, ..., Sn{this}; ... void actual_M_handler(mhood_t<M> cmd) {...} ... void so_define_agent() override { S1.event(&demo::actual_M_handler); ...
prevent()方法禁止事件处理程序搜索当前子状态及其所有父子状态。 假设我们有一个父状态A,其中在消息M上调用了std :: abort()。 在B的子状态下,可以安全地忽略M。 我们必须确定在子状态B中对M的反应,因为如果没有,则将在A中找到B的处理程序。因此,我们将需要编写以下内容:
void so_define_agent() override { A.event([](mhood_t<M>) { std::abort(); }); ... B.event([](mhood_t<M>) {});
prevent()方法使您可以在代码中更明确,更图形地编写这种情况:
void so_define_agent() override { A.event([](mhood_t<M>) { std::abort(); }); ... B.suppress<M>();
很简单的例子
SObjectizer v.5.5的标准示例包括一个简单的示例
blinking_led ,它模拟LED指示灯闪烁的操作。 此示例中的代理状态图如下:

这是此示例中的完整代理代码:
class blinking_led final : public so_5::agent_t { state_t off{ this }, blinking{ this }, blink_on{ initial_substate_of{ blinking } }, blink_off{ substate_of{ blinking } }; public : struct turn_on_off : public so_5::signal_t {}; blinking_led( context_t ctx ) : so_5::agent_t{ ctx } { this >>= off; off.just_switch_to< turn_on_off >( blinking ); blinking.just_switch_to< turn_on_off >( off ); blink_on .on_enter( []{ std::cout << "ON" << std::endl; } ) .on_exit( []{ std::cout << "off" << std::endl; } ) .time_limit( std::chrono::milliseconds{1250}, blink_off ); blink_off .time_limit( std::chrono::milliseconds{750}, blink_on ); } };
在这里,所有实际的工作都在眨眼间状态的I / O处理程序内完成。 好吧,还有,在blink_on和blink_off子状态下工作的持续时间限制。
这不是一个非常简单的例子
SObjectizer v.5.5的标准示例还包括一个更复杂的示例
intercom_statechart ,它模仿门电话面板的行为。 在此示例中,主代理的状态图如下所示:

一切都如此艰巨,因为这种模仿不仅支持按号码呼叫公寓,而且还支持诸如每个公寓的唯一密码以及特殊服务代码之类的事情。 这些代码允许您打开门锁而无需在任何地方拨号。
在此示例中仍然有一些有趣的事情。 但是它太大了,无法详细描述(即使单独撰写文章也可能不够)。 因此,如果您对复杂的ICA在SObjectizer中的外观感兴趣,可以在此示例中看到。 如果不清楚,您可以问我们一个问题。 例如,在对本文的评论中。
是否可以不使用对SObjectizer-5中内置的航天器的支持?
因此,SObjectizer-5具有对ICA的内置支持,并具有广泛的支持功能。 当然要提供此支持才能使用它。 特别是,SObjectizer的调试机制(如
消息传递跟踪 )会了解代理的状态,并在其各自的调试消息中显示当前状态。
但是,如果开发人员由于某种原因不希望使用内置的SObjectizer-5工具,则他可能不会这样做。
例如,您可以拒绝使用SObjectizer state_t和其他类似的对象,因为state_t是一个非常重的对象,内部带有std :: string,几个std :: function和几个计数器,例如std :: size_t,指向各种对象和其他一些琐事的五个指针。 例如,在64位Linux和GCC-5.5上,每个state_t总共提供160个字节(除了可以在动态内存中分配的字节)。
例如,如果您需要应用程序中的一百万个代理,每个代理将具有10个状态,则SObjectizer state_t的开销可能是不可接受的。 在这种情况下,您可以使用其他机制来处理状态机,将消息处理手动委派给该机制。 类似于:
class external_fsm_demo : public so_5::agent_t { some_fsm_type my_fsm_; ... void so_define_agent() override { so_subscribe_self() .event([this](mhood_t<msg_one> cmd) { my_fsm_.handle(*cmd); }) .event([this](mhood_t<msg_two> cmd) { my_fsm_.handle(*cmd); }) .event([this](mhood_t<msg_three> cmd) { my_fsm_.handle(*cmd); }); ... } ... };
在这种情况下,您将通过增加手工工作量以及缺乏SObjectizer调试机制的帮助来提高效率。 但是,这取决于开发人员来决定。
结论
原来,这篇文章篇幅庞大,远远超出了最初的计划。 感谢所有阅读这个地方的人。 如果其中一位读者认为可以将您的反馈留在文章的评论中,那就太好了。
如果仍然不清楚,请提出问题,我们将很高兴为您解答。
另外,借此机会,我想引起对SObjectizer感兴趣的人们的注意,该工作已经开始在分支5.5框架中开发下一版本的SObjectizer。简要介绍在5.5.23中考虑实现的内容,在此进行描述。更全面,但这里是英语。您可以对建议实施的任何功能发表意见,或提供其他功能。即
有机会影响SObjectizer的发展。此外,在v.5.5.23版本发布之后,SObjectizer上的工作可能会暂停,并且下一次机会可能无法在2018年的SObjectizer中包含一些有用的东西。