C/C++ 状态机实用技术 - 介绍

介绍

几乎所有的计算机系统,特别是嵌入式系统,都是事件驱动(Event-Driven)的,这意味着它们持续在等待一些外部或内部事件的发生,像时钟节拍、数据包到达、按钮按下或者鼠标点击等等。在识别到事件后,这类系统会通过执行适当的计算来做出响应,这些计算可能包括操纵硬件或产生 "软 "事件来触发其他内部软件组件。(这就是为什么事件驱动系统(Event-Driven System)又被称为反应式系统(Reactive System)的原因)。一旦事件处理完成,软件就会返回等待下个事件。

对于基本的顺序控制不用说你已经烂熟于心了,顺序式程序在它执行路径中各个点等待事件:手段包括主动地轮询事件、或者被动地阻塞在类似信号量这类的操作系统机制上。尽管这种风格的事件驱动系统编程方法在很多情况下是可行的,但是当潜在的事件源有多个、它们到达的时机和先后顺序无法事先预测、响应的及时性又很重要的时候,这种方法就不是很好用了。问题在于顺序程序在等待某种类型的事件时,它做不了任何其他工作,无法响应其他事件。

显然我们需要一种新的程序结构,能够对大量的潜在事件做出良好的响应。任何一个事件都可能在不可预知的时机、以不可预知的顺序到达。这种情况不仅在家用电器、手机、工业控制器、医疗设备等嵌入式系统中很常见,在现代桌面计算机中也是如此。想想用Web浏览器、文字处理器或电子表格的情况吧:这些程序大多有一个现代图形用户界面(Graphical User Interface,GUI)来提供处理多个事件的能力。所有现代GUI系统及许多嵌入式应用的开发者,都采用一种通用的程序结构以优雅地解决及时处理大量异步事件的问题。这种程序结构一般称为事件驱动编程(Event-Driven Programming)。

控制反转

事件驱动编程与传统的顺序程序需要明显不同的思维方式,后者以 "超级循环 "或传统RTOS中的任务为代表。几乎所有的现代事件驱动系统都是按照好莱坞原则来架构的,也就是奉行 "不要打给我我们,我们会打给你"的原则。所以,事件驱动式程序本身在等待事件时并不处于控制状态,事实上,它甚至没有在运行。只有在事件到达后,程序才会被调用以处理事件,完成后又迅速放弃控制权。这种结构使得事件驱动的系统可以并行地等待大量事件,以使系统对所有需要处理的事件都保持响应。

这种方案带来了三个重大好处:首先,它意味着事件驱动系统自然地分为应用程序和监督事件驱动的基础设施,前者负责实际处理事件,后者则等待事件并将其分派到应用程序。第二,因为控制驻留在事件驱动基础设施中,所以从应用的角度来看,控制上与传统的顺序程序相比是倒置的。第三,事件驱动的应用程序必须在处理完每个事件后交回控制权,所以执行上下文不能像顺序程序那样保留在基于堆栈的变量和程序计数器中。相反,事件驱动的应用程序变成了一个状态机,或者实际上是一组协作的状态机,在静态变量中保存了从一个事件到下一个事件的上下文。

事件驱动框架的重要性

控制权倒置,在所有事件驱动系统中都如此典型,使得事件驱动基础设施具备了一个应用框架(Application Framwork)所有定义特征,而不是一个工具包。当你使用一个工具包的时候,像传统的操作系统或RTOS,你编写的是应用程序的main函数体,并调用你想复用的工具包代码。而当你使用框架时,你复用main函数体,编写的是被mian函数体调用的代码。

另一个重要的点是,如果你想把多个事件驱动的状态机组合成系统,一个事件驱动的框架实际上必不可少。要执行并发状态机,所需的东西比区区一个传统RTOS中的API要多。

状态机需要一个基础设施或者叫框架,它至少要为每个状态机提供运行到完成(Run-to-Complete,RTC)的执行上下文、事件排队和基于事件的定时服务。这是真正的关键点。状态机无法运行在真空中,如果没有事件驱动的框架,状态机是不具备真正的实用性的。

主动对象计算模型

本书汇集了解耦事件驱动系统的两种最有效的技术:分层状态机和事件驱动框架。这两个元素的组合被称为主动对象计算模型(Active Object Computing Model)。主动对象(Active Object)一词来自UML,表示一个自主的对象通过事件异步地与其他主动对象交互。UML进一步提出了状态图的UML变体,用于建模事件驱动的主动对象的行为。

在本书中,主动对象是通过名为QF的事件驱动框架来实现的,它是QP事件驱动平台的主要组成部分。QF框架有序地执行主动对象,并负责主动对象中线程安全的事件交换和处理的所有细节。QF通过对事件进行排队并依次(一次一个)调度到活动对象的内部状态机上,确保了状态机执行所普遍依赖的RTC语义。

分层状态机与事件驱动框架相结合的基本概念并不新鲜。事实上,它们至少已经被广泛使用了20年。几乎目前市场上所有成功的商业设计自动化工具,都基于分层状态机(statecharts)、并在内部集成了类似于QF的事件驱动的实时框架。

以代码为中心的方法

我在本书中采用的方法是以代码为中心、保持极简和偏底层的。这个特点并不是贬义的,它只是意味着你得学习如何在没有大型工具辅助的情况下,直接将分层状态机和活动对象映射为C或C++源代码。在这里工具不是问题--问题是理解机制本身。

现代的设计自动化工具确实很强大,但它们并不适合所有人。对很多开发人员来说,工具在还没发挥出作用之前就被放弃了。本书中介绍的以代码为中心的方法,可以为这些开发人员,提供一个重量级工具的轻量级替代方案。

但最重要的是,任何工具都无法取代概念上的理解。比如,在一个明确的状态迁移中,确定退出和进入的动作及其顺序,不应该是通过在工具中运行一个支持的状态机动画来完成的。它应该是基于你对底层状态机实现的理解(在第3章和第4章中讨论)来给出答案。即使你以后决定使用设计自动化工具,即使那个特定的工具会使用与本书讨论的状态图实现技术有差别,你仍然会因为你对基本机制的底层理解而更有信心、更有效地应用这些概念。

尽管有很多来自现有用户的压力,我还是坚持保持QP 事件驱动平台的精简,只对庞大的UML规范中的基本元素提供直接实现,并将那些美好的东西作为设计模式来支持。保持核心实现的小而简单能带来实际的好处。开发者可以快速学习和部署QP,而无需在工具和培训上进行大量投资。他们可以很容易地适配和定制框架的源代码,以满足其特定的情境,包括资源严重受限的嵌入式系统。他们可以理解并经常使用框架提供的所有功能。

关注解决实际问题

你不能只把状态机和事件驱动框架看成是功能的集合,因为有些功能孤立地看是没有意义的。只有当你在考虑设计而不是简单的编码时,你才能有效地使用这些强大的概念。而要想这样理解状态机,你必须了解事件驱动编程的一般问题。

本书讨论了事件驱动编程的问题,为什么会成为问题,以及状态机和主动对象计算模型如何帮助解决这些问题。因此,我在大多数章节的开头都会介绍本章要解决的编程问题。通过这样的方式,我希望能让你逐步地达到这样的程度:能够将分层状态机和事件驱动框架做为一种更自然的解决问题的方式,而不是使用传统的方法,类似深度嵌套的IF和ELSE来编码有状态的行为,或者通过传统RTOS的信号量或事件标志来传递事件等。

面向对象

尽管我使用C语言作为主要的编程语言,但我也广泛地使用了面向对象的设计原则。像几乎所有的应用框架一样,QP使用封装(类)和单一继承的基本概念作为定制、特化和扩展框架到特定应用的主要机制。如果这些概念对你来说很陌生,尤其你只会C语言,不要担心,在C语言层面上,封装和继承只是成为简单的编码习惯,我在第1章中介绍了这些概念。在C语言版本中,我特意避开了多态性,因为在C语言中实现后期的绑定,会引入不必要的复杂。当然,C++版本就是直接使用类和继承,QP/C++应用也是可以使用多态的。

更多乐趣

当你开始使用本书中描述的技术时,你的问题将发生改变。你将不用再跟嵌套15层的if-else语句搏斗了,也不用再担心信号量或类似的RTOS底层机制。取而代之的,你将开始在更高的抽象层次上思考状态机、事件和主动对象。当你经历了这个量子跃迁之后,你会发现,就像我一样,编程可以变得更加有趣。你再也不想回到 "意大利面条 "代码或原始RTOS上去了。

如何联系我

如果你对本书、代码或一般的事件驱动编程有意见或问题,我很乐意听到你的意见。请给我发电子邮件:miro@state-machine.com

留下你的脚步
推荐阅读