c++如何利用协程实现一个状态机? (C++20 coroutine)

协程状态机的核心是“状态转移控制权”而非“挂起”,需自定义promise_type接管流转逻辑,通过final_suspend返回suspend_always防止销毁、unhandled_exception处理

异常、next_state驱动跳转,并由驱动循环显式resume推进状态。

协程状态机的核心不是“挂起”,而是“状态转移控制权”

用 C++20 协程写状态机,关键不在 co_await 本身,而在如何把每个状态封装成可恢复的执行片段,并让状态跳转由协程的恢复点(resume point)自然承载。标准库不提供现成的状态机协程框架,必须自己设计 promise_type 来接管状态流转逻辑。

必须自定义 promise_type 并重载 unhandled_exception 和 final_suspend

默认的 std::coroutine_handle 无法支撑状态机语义——你不能让协程在状态结束时自动销毁(否则无法做状态迁移),也不能任由异常崩溃掉整个状态流。需要:

  • final_suspend 返回 std::suspend_always{},防止协程彻底销毁,保留 handle 供外部驱动下个状态
  • unhandled_exception 至少记录或转发异常,避免静默终止(状态机里异常常意味着非法转移)
  • promise_type 中添加 next_state 成员(如 std::function 或枚举),并在 await_resume() 后主动调用它
struct state_machine_promise {
    std::function next_state;
    auto get_return_object() { return state_machine{this}; }
    auto initial_suspend() { return std::suspend_always{}; }
    auto final_suspend() noexcept { return std::suspend_always{}; }
    void unhandled_exception() { /* 记录异常,不抛出 */ }
    void return_void() {}
    auto await_transform(auto&&) { return simple_awaiter{*this}; }
};

用 awaiter 控制状态进入与退出时机

每个状态对应一个 co_await 表达式,但它的 awaiter 不该只做“等待”,而应完成三件事:保存当前状态上下文、触发退出逻辑(如清理资源)、设置 next_state。常见错误是把所有状态逻辑塞进一个协程函数里,结果变成难以调试的“大面条”。

  • 每个状态建议拆成独立函数,返回 state_machine 类型(包装了 coroutine_handle
  • simple_awaiter::await_ready() 应始终返回 false,确保每次 co_await 都真实挂起,把控制权交还给驱动循环
  • 不要依赖 co_yield 实现状态跳转——它语义是“产出值并挂起”,和状态机的“执行→转移”模型不匹配

驱动循环必须显式 resume 并检查状态终止

协程状态机没有内置调度器,你得自己写一个运行循环来推进状态。容易忽略的是:不能无条件 handle.resume(),必须先检查 handle.done(),否则对已结束协程 resume 会触发未定义行为(UB)。

void run_state_machine(state_machine sm) {
    auto h = sm.handle;
    while (!h.done()) {
        h.resume();
        // 此处可插入日志、超时检查、事件分发等
    }
}

真正复杂的地方在于状态间的数据传递——promise_type 是唯一共享上下文的位置,所有状态共用同一份 promise 实例,变量生命周期必须严格匹配整个状态机周期,局部变量不能跨 co_await 存活(除非用 static 或堆分配)。