协程、状态机与任务队列
Category
server / runtime 子树。节点定位:协程、状态机、任务队列回答三种不同问题——异步编排、显式时序、排队与治理;分工要和执行模型配合看。
Definition
协程、状态机和任务队列,看起来像三种不同的“写代码方式”,实际上它们分别在回答三种不同问题:
- 协程:异步流程怎样写得自然。
- 状态机:流程阶段怎样表达得清楚。
- 任务队列:工作单元怎样排队、限流和消费。
很多系统把这三件事混成一件事,最后不是异步流程写得像回调迷宫,就是队列堆爆后没人知道该丢什么、该停什么。
Problem
三者最易混淆:协程带来异步观感、状态机维护显式分支、队列负责排队治理,混用会把可推理的时序写成隐式纠缠。分工先定,「什么时候优先谁」才有可判断的依据。
协程真正擅长什么
协程最大的价值,不是“轻量线程”这句口号,而是它能把异步流程写得接近顺序代码。适合的场景通常包括:
- 连接与会话流程。
- 资源加载和异步等待。
- 多步骤但状态不复杂的业务流程。
- 需要等待外部 I/O 结果的链路。
它的优势很直接:
- 写法自然。
- 逻辑上下文连续。
- 错误处理相对集中。
但协程不天然等于安全。它最容易掩盖的问题是:
- 一个协程挂起后,恢复时状态是否还有效。
- 协程数量是否无边界增长。
- 协程执行点分散,难以看出真正调度时序。
所以协程更适合表达流程,不适合替代状态治理。
为什么显式状态机仍然重要
很多团队一用了协程,就不愿意写状态机了。这个倾向在简单流程里可以理解,在复杂交互里往往很危险。显式状态机最大的价值是:
- 状态集合可见。
- 迁移条件可见。
- 非法状态转换容易检查。
- 恢复和回放更容易落地。
典型适合状态机的场景包括:
- 房间生命周期。
- 匹配状态流转。
- 技能施法阶段。
- 支付订单状态。
- 玩家重连与托管切换。
协程可以把流程写顺,状态机可以把阶段写清。两者不是互斥关系。
任务队列解决的不是异步,而是排队与治理
任务队列最容易被误用成“凡是异步都丢队列”。实际上,任务队列真正擅长的是:
- 把工作单元排队。
- 做削峰填谷。
- 做优先级隔离。
- 做限流、重试和失败转移。
这在游戏系统里尤其重要,因为你会反复遇到:
- 登录高峰。
- 活动高峰。
- 大量奖励发放。
- 大量异步持久化。
- 批量日志和埋点。
任务队列的本质,是稳定性工具,不是写法工具。
这三者最容易混淆的地方
协程写成了隐式状态机,但没人知道状态在哪
流程看起来很顺,但一旦中断、重试、回放或恢复,就很难说清当前阶段在哪里。
状态机被写成 if-else 栈
名字上叫状态机,实际上只是大量分支和布尔标记堆在一起,没有真正的状态集合和迁移语义。
队列只是堆积器
消息和任务都能进队列,但没有容量上限、没有优先级、没有超时和丢弃策略。这样队列会从缓冲层变成故障放大器。
一个更实用的分工方式
比较稳妥的做法通常是:
- 协程用来表达等待和异步步骤。
- 状态机用来描述有限阶段和状态迁移。
- 任务队列用来承载可排队、可延后、可治理的工作单元。
例如:
- 玩家登录流程可以用协程串起来。
- 玩家会话状态切换应该用状态机约束。
- 登录后的日志落盘、外围通知和某些异步回写可以交给任务队列。
一旦这样分工,系统的运行时语义会清楚很多。
什么时候该优先哪一种
协程优先
当流程是线性的、等待点明确、状态数量有限,而且主要复杂度来自异步等待时,协程通常最顺手。
状态机优先
当流程有明显阶段、会频繁中断恢复、需要可视化调试或强校验时,状态机通常更稳。
队列优先
当真正的问题是削峰、限流、异步消费和失败治理,而不是流程表达时,任务队列才是主角。
它们必须和执行模型配合起来看
这三者并不是独立于线程模型存在的,它们会和执行模型一起决定系统行为:
- 单线程主循环里跑协程,能保持较强时序可推理性。
- Actor 内部用状态机管理生命周期,往往比散乱回调更稳。
- 多线程环境里任务队列必须和背压、容量、优先级一起设计。
也就是说,它们是运行时组织的局部工具,而不是替代整体执行模型。
常见误区
- 把协程当成异步银弹,结果系统里到处是不可见状态和悬挂流程。
- 名义上用了状态机,实际上仍靠布尔变量和分支堆逻辑。
- 把任务队列只当缓存池,不做容量、优先级、超时和丢弃策略,最后高峰期队列反而成为故障放大器。
这一页真正要强调的,不是三种写法谁更优雅,而是异步流程、有限状态和排队治理应该由不同机制分别负责。
实战案例:一次「重连后订单状态错乱」的隐式状态与队列堆积排查
案例为教学示例(非摘自真实项目),数字经过取整简化。它把本页「为什么显式状态机仍然重要」「任务队列解决的不是异步,而是排队与治理」「一个更实用的分工方式」用到深水区:流程写得顺,状态却没人说得清——这是协程、状态机、队列混成一件事的排查,也是「怎样让异步流程、有限状态和排队治理各归其位」的完整展开。
背景与现象:某手游的支付与订单流程全部用协程串起。上线后出现两类问题:重连后订单状态错乱,日 60 单;高峰活动时「奖励发放延迟 20 分钟」。两类问题的共同点是——出问题的地方都没有一个能说清「现在处在哪个状态」的东西。
第一步:现象与口径——先分清是状态问题还是排队问题
状态口径:订单状态散落在协程的局部变量、布尔标记和回调闭包里,重连或超时重试后无法重建当前阶段,客服只能凭日志猜。
队列口径:奖励发放任务队列无容量上限、无优先级,高峰堆积到 4.2 万条,尾延迟 20 分钟;低优先级的活动通知和高优先级的付费奖励混在同一队列。
第二步:分层归因——按本页三条混淆点比对
故障归因 占比
协程写成隐式状态机,中断后状态无法重建 55%
名义状态机实为布尔标记堆叠,非法转换无校验 25%
任务队列只当缓存池,无容量/优先级/超时 20%逐项比对:订单流程「看起来很顺」,但一中断、重试、重连就说不清阶段——命中本页第一条混淆点;订单状态用几个布尔标记拼出来,非法转换没有校验——命中第二条;奖励队列只有入队没有治理——命中第三条,队列会从缓冲层变成故障放大器。
第三步:根因——三件事混成了一件
流程表达(协程)、阶段治理(状态机)、排队治理(队列)没有分工:协程既当流程又当状态容器,状态机只是分支堆叠,队列只是堆积器。三种机制各自的职责没有被分开,系统运行时语义自然就糊。
第四步:处置与回填——三件事分开,各归其位
- 订单状态抽成显式状态机:状态集合、迁移条件、非法转换校验都写出来,重连后按当前状态重建,不再依赖协程局部变量。
- 协程只表达等待步骤:把「等待支付回调」「等待发货结果」这类异步步骤留在协程里,阶段判断交给状态机。
- 奖励队列补治理四件套:容量上限、优先级隔离(付费奖励优先于活动通知)、超时丢弃、失败转移;队列满时低优先级任务降级延后。
复核账:订单状态错乱日 60 单 → 0 单;奖励发放尾延迟 20 分钟 → 90 秒;队列峰值 4.2 万条 → 6000 条。
回填清单:
| 回填项 | 动作 | 回填处 |
|---|---|---|
| 显式状态机 | 状态集合与迁移条件写出来,非法转换校验,重连可按状态重建 | 为什么显式状态机仍然重要 |
| 协程职责边界 | 协程只表达等待与异步步骤,不承担状态治理 | 协程真正擅长什么 |
| 队列治理四件套 | 容量上限、优先级隔离、超时丢弃、失败转移逐项落地 | 任务队列解决的不是异步,而是排队与治理 |
| 分工方式 | 协程管流程、状态机管阶段、队列管排队,写进设计规范 | 一个更实用的分工方式 |
案例的三个教训
- 流程写得顺,不等于状态说得清。协程擅长表达等待,不擅长承载状态;中断、重试、重连一来,隐式状态就现原形。
- 名义状态机要真有状态集合和迁移语义。几个布尔标记堆出来的「状态机」挡不住非法转换,也撑不起恢复和回放。
- 队列没有治理就是故障放大器。容量、优先级、超时、失败转移缺一样,高峰期的队列就从缓冲层变成放大器。
Used By
异步编排选型;状态机与队列的职责划分。
- Leaf(commit
af71eb0):协程与队列分工的现成样板——Skeleton 主循环单 select 六路复用(closeSig、异步回调、ChanRPC、命令、Go 回调、定时器)(module/skeleton.go:45-68),跨模块经 ChanRPC 队列进出(chanrpc/chanrpc.go:13-21,Go异步:138、Call0阻塞:155),每模块一个 goroutine(module/module.go:40)
Related
关系链:协程 → 状态机 → 任务队列 → 混淆点 → 分工 → 优先级 → 配合执行模型。上游:runtime/01。下游:runtime/03(Tick 与异步配合)。
Reference
本页为工程经验归纳;页内实战案例为教学示例(数字取整简化)。