【问题标题】:Event based state machine in c++ with coroutines带有协程的 C++ 中基于事件的状态机
【发布时间】:2015-10-12 20:43:16
【问题描述】:

C++ 中的协程是一种非常强大的实现状态机的技术,但是我在 Internet 上找到的示例过于简单,例如它们通常代表某种迭代器,在调用某些“Next”例程之后移动,仅依赖于协程的初始参数。然而,在相当复杂的基于事件的状态机中,每个下一步都取决于接收到的导致恢复运行的特定事件,并且还应该为可能随时发生的事件实现一些默认事件处理程序。

假设我们有一个简单的电话状态机。

STATE:HOOK OFF-->[EVT:DIAL TONE]--> [STATE:DIALING] --> [EVT: NUMBER DIALED] --> STATE:TALKING。

现在我想要一个可以看到类似内容的协程。

PhoneSM()
{ 
HookOf(); 
Yield_Till(DialTone_Event); 
Dial(); 
Yield_Till(EndOfDial_Event); 
Talk(); 
...
}

例如要求

  1. Yield_Till 仅在接收到特定事件时才会继续(如何???)当 couroutine 运行恢复时。如果没有,那么它应该再次屈服。

  2. Yield_Till 必须知道如何将事件运行到像 Hangup_Event 这样的默认处理程序,因为实际上它可以随时发生,而且每次都添加它会很麻烦。

任何有关 c++(仅限!!!)实现或满足要求的现成基础架构的帮助将不胜感激。

【问题讨论】:

    标签: c++ coroutine state-machine


    【解决方案1】:

    在我看来,您正在尝试将事件驱动的状态机编码为顺序流程图。 state-charts 和 flow-charts 之间的区别是非常基本的,并在文章中进行了解释,例如"A Crash Course in UML State Machines"

    状态机应该被编码为一次性函数,它处理当前事件并返回而不产生或阻塞。如果你想使用协程,你可以从协程中调用这个状态机函数,然后在每个事件之后产生。

    【讨论】:

      【解决方案2】:

      大多数协程库不会关闭复杂的 yield 函数。他们只是让步,你的协同程序将在某个任意点重新获得控制权。因此,在 yield 之后,您必须在代码中测试适当的条件,如果不满足则再次 yield。在此代码中,您还可以对挂断等事件进行测试,在这种情况下,您将终止协同程序。

      在公共领域有许多实现,一些操作系统(例如 Windows)提供协程服务。只需 google 一下 co-routine 或 Fiber。

      【讨论】:

        【解决方案3】:

        这是一个老问题,但我在搜索如何使用 C++20 协程实现这一目标时发现的第一个问题。由于我已经用不同的方法实现了几次,我仍然尝试为未来的读者回答它。

        首先了解一下为什么这实际上是一个状态机的背景。如果您只对如何实现它感兴趣,您可以跳过这部分。状态机被引入作为一种标准方式来执行代码,该代码不时调用新事件并推进一些内部状态。在这种情况下,程序计数器和状态变量显然不能存在于寄存器和堆栈中,需要一些额外的代码才能从你离开的地方继续。状态机是实现这一目标的标准方法,无需过多开销。然而,可以为相同的任务编写协程,并且每个状态机都可以在这样的协程中传输,其中每个状态都是一个标签,事件处理代码以转到下一个状态结束,此时它产生。每个开发人员都知道 goto-code 是意大利面条式代码,并且有一种更简洁的方式来表达使用流控制结构的意图。事实上,我还没有看到无法使用协程和流控制以更紧凑、更易于理解的方式编写的状态机。话虽如此:这如何在 C/C++ 中实现?

        有几种实现协程的方法:可以使用循环中的 switch 语句来完成,例如 Duff's device,有 POSIX coroutines 现在已过时并从标准中删除,C++20 带来了现代基于 C++ 的协程。为了拥有一个完整的事件处理状态机,还有一些额外的要求。首先,协程必须产生一组将继续它的事件。然后需要有一种方法将实际发生的事件连同它的参数一起传回协程。最后必须有一些驱动程序代码来管理事件并在等待的事件上注册事件处理程序、回调或信号槽连接,并在此类事件发生时调用协程。

        在我最新的实现中,我使用了驻留在协程内并由引用/指针产生的事件对象。通过这种方式,协程能够决定何时对此类事件感兴趣,即使它可能不处于能够处理它的状态(例如,对先前发送请求的响应得到答复但答案不是待处理)。它还允许使用不同的事件类型,这些事件类型可能需要不同的方法来侦听独立于使用的驱动程序代码的事件(可以通过这种方式进行简化)。

        这是问题中状态机的一个小型 Duff 设备协程(带有用于演示目的的额外占用事件):

        class PhoneSM
        {
            enum State { Start, WaitForDialTone, WaitForEndOfDial, … };
            State state = Start;
            std::unique_ptr<DialTone_Event> dialToneEvent;
            std::unique_ptr<EndOfDial_Event> endOfDialEvent;
            std::unique_ptr<Occupied_Event> occupiedEvent;
        public:
            std::vector<Event*> operator()(Event *lastEvent = nullptr)
            {
                while (1) {
                    switch (state) {
                    case Start:
                        HookOf();
        
                        dialToneEvent = std::make_unique<DialTone_Event>();
                        state = WaitForDialTone;
                        // yield ( dialToneEvent )
                        return std::vector<Event*>{ dialToneEvent.get() };
                    case WaitForDialTone:
                        assert(lastEvent == dialToneEvent);
                        dialToneEvent.reset();
        
                        Dial();
        
                        endOfDialEvent = std::make_unique<EndOfDial_Event>();
                        occupiedEvent = std::make_unique<Occupied_Event>();
                        state = WaitForEndOfDial;
                        // yield ( endOfDialEvent, occupiedEvent )
                        return std::vector<Event*>{ endOfDialEvent.get(), occupiedEvent.get() };
                    case WaitForEndOfDial:
                        
                        if (lastEvent == occupiedEvent) {
                            // Just return from the coroutine
                            return std::vector<Event*>();
                        }
                        assert(lastEvent == endOfDialEvent);
                        occupiedEvent.reset();
                        endOfDialEvent.reset();
        
                        Talk();
        
                        …
                    }
                }
            }
        }
        

        当然,实现所有协程处理会使这变得过于复杂。真正的协程会简单得多。以下为伪代码:

        coroutine std::vector<Event*> PhoneSM() {
            HookUp();
            {
                DialToneEvent dialTone;
                yield { & dialTone };
            }
            Dial();
            {
                EndOfDialEvent endOfDial;
                OccupiedEvent occupied;
                Event *occurred = yield { & endOfDial, & occupied };
                if (occurred == & occupied) {
                    return;
                }
            }
            Talk();
            …
        }
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2020-08-02
          • 2014-10-24
          • 2011-01-05
          • 2018-05-29
          • 1970-01-01
          • 2018-07-16
          • 2023-03-17
          • 1970-01-01
          相关资源
          最近更新 更多