协程不能直接替代状态机,但能大幅简化状态流转逻辑;需自定义promise_type承载分布式上下文、避免共享状态、区分co_await语义层级、精准控制checkpoint粒度。

协程不能直接替代状态机,但能大幅简化状态流转逻辑
很多人以为把 async/await 一加,状态机就自动消失了——实际恰恰相反:C++20 协程不管理状态,它只是让「等待某个异步事件并继续执行」这件事不再需要手写 switch + state + goto。真正的状态逻辑仍需你定义,但协程让你能把「当前在哪个步骤、等什么、下一步做什么」这些原本散落在回调和成员变量里的东西,收束到一个线性函数体内。
关键判断是:如果你的状态机里大量出现 on_response_received → 更新 m_state → 调用 handle_next_step() 这类跳转链,那协程改造就值得投入;如果状态转移依赖外部强干预(比如多节点并发修改同一状态),协程反而会掩盖竞态,此时应优先加固同步而非强行协程化。
必须自定义 promise_type 来承载分布式上下文
标准库的 std::suspend_always 或 std::suspend_never 对分布式场景完全无用:你没法在挂起时保存请求 ID、超时时间、重试次数、节点路由信息。所有这些必须塞进自定义 promise_type 里,并确保它满足可迁移(movable)、可序列化(至少部分字段)。
-
promise_type::get_return_object()应返回一个带request_id()和node_id()访问接口的 handle 封装 -
promise_type::await_transform()需重载对网络调用(如rpc_call<t>()</t>)的支持,自动注入当前 promise 的上下文(如 trace_id、deadline) - 挂起前务必调用
save_to_checkpoint()(你自己实现)把关键状态写入本地日志或 etcd,否则崩溃后无法恢复
避免在协程内直接操作共享状态变量
常见错误是把原来状态机的 m_current_state、m_pending_request 等成员变量照搬进协程函数体,然后在 co_await rpc_call(...) 前后读写它们——这在单线程下看似可行,但在分布式重试或 failover 场景中,同一协程可能在不同机器上恢复执行,而成员变量只存在于原始对象内存里,早已丢失。
立即学习“C++免费学习笔记(深入)”;
正确做法是:
- 所有需跨恢复点存活的数据,必须显式存入
promise_type成员,且该成员支持序列化/反序列化 - 避免使用
this指针访问非 const 成员函数;若必须,需确保对象生命周期覆盖整个协程(例如用std::shared_ptr<self></self>持有自身) - 对全局状态(如连接池、metrics 计数器)的修改,统一走原子操作或带版本号的 CAS,不要依赖协程执行顺序
co_await 的 suspend/resume 不等于网络 IO 完成,必须区分语义层级
一个典型陷阱是认为 co_await rpc_call("node-A", req) 返回时,远程节点一定已处理完请求。实际上它只表示「本地发包完成且收到响应(或超时)」,而分布式系统中「响应到达」和「对方真正 commit 完成」之间存在 gap。这意味着你的状态机逻辑仍要处理幂等、重复响应、乱序响应。
实操建议:
- 每个
co_await后必须检查响应中的status_code和seq_no,不能只看是否抛异常 - 对关键状态变更(如“订单从 created → paid”),在协程内只做预提交(
prepare_transition("paid")),等收到对方 confirm 后再commit_transition() - 编译期加
static_assert检查所有co_await表达式类型是否实现了await_ready()/await_resume()显式契约,防止误用裸 future
最易被忽略的是 checkpoint 的粒度:不是每个 co_await 后都存盘,而是仅在状态真正发生不可逆变更(如写入数据库成功、发送 ack 到下游)之后才落盘。存太勤拖慢性能,存太少导致状态丢失。这个边界得结合业务语义手工界定,没有通用解。


















