C 语言状态机介绍

本贴最后更新于 225 天前,其中的信息可能已经时移世易

介绍

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

对于基本的顺序式控制你无疑已经习以为常了,顺序式程序在它执行路径中的各个地方等待着事件:通过主动轮询事件,或者被动地阻塞在信号量等类似的操作系统机制上。尽管这种风格的事件驱动系统的编程方法在很多情况下是可行的,但是当存在多个到达时机和顺序是你无法预计的潜在事件源、并且对这些事件及时处理又很重要的时候,这种方法就不是很好用了。问题就在于,当一个顺序式程序在等待某种类型的事件的时候,它做不了任何其他工作,对其他类型事件也无法做出响应。

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

控制反转

事件驱动编程所需的思维方式,与"超级循环"或者传统 RTOS 中的任务等这种顺序式程序,有着明显的不同。大多数现代的事件驱动系统都是按照好莱坞原则来架构的,也就是说 "不要打给我们,我们会打给你"。所以,事件驱动的程序在等待事件时并不处于控制状态,事实上,它甚至不是活动的。只有在事件到来后,程序才会被调用处理事件,然后它又迅速放弃控制权。这种安排允许事件驱动的系统并行地等待许多事件,因此系统对它需要处理的所有事件都保持响应。

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

事件驱动框架的重要性

控制权的倒置,在所有事件驱动的系统中都是如此典型,这让事件驱动基础设施更符合一个应用框架的所有特征,而不是一个工具包。当你使用一个工具包时,比如传统的操作系统或 RTOS,你编写应用程序的主体,并调用你想重用的工具包代码。当你使用框架时,你重用 main 函数体,并编写被 main 函数调用的代码。

另一个要点是,如果你想把多个事件驱动的状态机组成一个系统,一个事件驱动的框架实际上是必不可少的。要执行并行的状态机,确实需要的不仅仅是"一个"像传统的 RTOS 中的 API。

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

主动对象计算模型

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

在本书中,主动对象是使用名为 QF 的事件驱动框架来实现的,它是 QP 事件驱动平台的主要组成部分。QF 框架有序地执行主动对象,并负责线程安全的事件交换和处理的所有细节。QF 保证了状态机执行的普遍假设的 RTC 语义,这是通过对事件进行排队并将其串行(逐个)地分派到主动对象内部的状态机实现的。

分层状态机与事件驱动框架相结合的基础概念并不是什么新鲜事物。事实上,它们已经被广泛使用了至少 20 年。基本上目前市场上所有成功的商业设计自动化工具都是基于分层状态机(状态图)的,并在内部结合类似 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。

  • 状态机
    4 引用 • 1 回帖
  • C

    C 语言是一门通用计算机编程语言,应用广泛。C 语言的设计目标是提供一种能以简易的方式编译、处理低级存储器、产生少量的机器码以及不需要任何运行环境支持便能运行的编程语言。

    62 引用 • 163 回帖 • 366 关注

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
  • devcui
    捐赠者

    先立一个道标