这是一个老问题,但我在搜索如何使用 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();
…
}