Skip to content

单线程、多线程与 Actor ​

Category ​

server / runtime 子树。节点定位:执行模型首先是状态问题——单线程、多线程、Actor 是三种状态组织哲学,真实系统是混合式不是三选一。

Definition ​

单线程、多线程和 Actor,表面上像三种并列技术选项,实际上它们对应的是三种完全不同的状态组织哲学。真正要比较的从来不是“谁更快”,而是:

  • 状态由谁持有。
  • 调度单位是什么。
  • 并发冲突怎样避免。
  • 高峰和故障时哪种模型更容易推理。

很多游戏系统线上不稳定,不是因为算法太差,而是执行模型和状态边界从一开始就没对上。

Problem ​

比较执行模型若问「谁更快」就问错了:三种模型对应完全不同的状态组织与推理方式,单线程胜在可推理、多线程胜在真并行、Actor 胜在隔离。真实系统几乎都是混合式,选型落点是游戏服务器的常见形态而非理论优劣。

为什么执行模型首先是状态问题 ​

讨论并发模型时,最常见的误区是只盯线程数和 CPU 利用率。实际更重要的问题通常是:

  • 同一份状态能不能被多人同时改。
  • 一条业务链路的执行顺序是不是稳定。
  • 异步任务回来以后由谁接管结果。
  • 某个模块慢下来会不会拖垮全局。

这些问题本质上都和状态归属有关,而不是和“用了几个线程”有关。

单线程为什么长期有生命力 ​

很多游戏逻辑,尤其是房间、场景、战斗、撮合、状态机这类核心链路,依然偏爱单线程或单写者模型。不是因为技术落后,而是因为单线程天然解决了很多最难的问题:

  • 执行顺序稳定。
  • 没有锁竞争。
  • 状态变更时序容易推理。
  • 回放、重演和调试成本低。

单线程真正适合的是:

  • 状态强耦合、需要顺序执行的逻辑。
  • 单实例内负载还在可控范围。
  • 故障成本更怕不一致,而不是怕 CPU 没打满。

它的代价也非常清楚:单实例吞吐有上限,热点集中时需要靠分区、拆实例或异步外包来扩展,而不是靠“再开几个线程”硬顶。

多线程真正擅长什么 ​

多线程不是默认更高级,它真正擅长的是:

  • 并行重计算。
  • 异步 I/O。
  • 批处理任务。
  • 和主逻辑弱耦合的可并行工作。

例如:

  • 路径搜索批处理。
  • 资源预处理。
  • 日志落盘。
  • 外围统计。
  • 编码压缩。

多线程最不适合的是共享状态非常紧、顺序要求非常强、结果必须严格可重放的核心逻辑。因为一旦这些逻辑跨线程自由流动,锁竞争和时序问题会迅速放大。

Actor 到底解决了什么 ​

Actor 最有价值的地方,不是“消息听起来很优雅”,而是它把并发问题改写成了:

  • 每个 Actor 持有自己的封闭状态。
  • 外部通过消息交互,而不是直接共享内存。
  • 同一个 Actor 内通常顺序执行。

这带来的收益非常直接:

  • 状态边界清楚。
  • 并发冲突减少。
  • 模块之间依赖更显式。
  • 比完全共享内存模型更容易控制故障扩散。

Actor 特别适合:

  • 有天然分区键或单写者模型的系统。
  • 房间、场景、玩家实体、撮合分桶和顺序状态机。
  • 想把“线程并发”转成“消息协作”的团队。

但 Actor 也不是免费午餐。你仍然要解决:

  • Actor 之间的消息风暴。
  • 邮箱积压。
  • 热点 Actor 成为瓶颈。
  • 跨 Actor 协作和一致性。

更实用的比较方式应该看什么 ​

状态隔离 ​

  • 单线程:最强,默认都在同一上下文里。
  • 多线程:最弱,除非额外治理状态边界。
  • Actor:局部最强,前提是状态真的被封闭在 Actor 内。

吞吐扩展 ​

  • 单线程:靠拆实例、分区和外包重任务扩展。
  • 多线程:理论扩展最好,但真实收益高度依赖任务独立性。
  • Actor:靠多 Actor、分区和调度器扩展,但热点 Actor 会限制上限。

时序可推理性 ​

  • 单线程:最好。
  • 多线程:最差。
  • Actor:介于两者之间,单 Actor 内较好,跨 Actor 需额外治理。

线上可恢复性 ​

  • 单线程:故障定位最直观,但热点压力集中。
  • 多线程:问题最容易隐蔽,尤其是偶发时序 Bug。
  • Actor:局部故障更容易隔离,但消息队列和热点分布必须被持续观察。

真实系统通常是混合式,而不是三选一 ​

成熟系统几乎不会只选一种模型,更常见的是:

  • 核心状态机和战斗房间用单线程或单写 Actor。
  • 资源处理、压缩、日志、外围计算用多线程。
  • 异步流程由协程或任务队列承接。

关键不在于“混搭”,而在于明确哪些部分必须顺序,哪些部分值得并行,哪些部分可以异步化但不能反向污染核心状态。

一个更贴近游戏服务器的常见落点 ​

比较稳妥的工程落点通常是:

  • 网关和 I/O 层采用事件驱动或多线程 I/O。
  • 房间、场景、玩家状态采用单写线程或 Actor 分区。
  • 重计算任务用线程池或批处理系统外包。
  • 长期资产和外围系统尽量异步化。

这种组织方式不是唯一答案,但它符合一个核心原则:把最怕不一致的状态,留在最容易推理的执行模型里。

常见误区 ​

  • 为了性能过早并发化,结果共享状态和锁竞争把问题放大。
  • 把 Actor 当成自动解决一切并发问题的银弹,却不治理热点 Actor、邮箱积压和跨 Actor 一致性。
  • 只盯 CPU 利用率,不看时序可推理性、恢复难度和线上排障成本。

这一页真正要回答的,不是哪种模型更潮,而是核心状态到底应该待在什么样的执行单元里。

实战案例:一次「为了性能上了多线程,时序 Bug 反而查不清」的并发化误判排查 ​

案例为教学示例(非摘自真实项目),数字经过取整简化。它把本页「为什么执行模型首先是状态问题」「多线程真正擅长什么」「一个更贴近游戏服务器的常见落点」用到深水区:吞吐确实涨了,时序故障却查不清——这是为性能过早并发化的排查,也是「怎样把最怕不一致的状态留在最容易推理的执行模型里」的完整展开。

背景与现象:某 SLG 战斗服,单实例承载 400 个房间。团队为提升 CPU 利用率,把房间逻辑从单线程改成 4 线程共享同一份世界状态。上线后平均 TPS 提升 22%,但每周出现 5–8 起时序类故障——技能结算偶发重复或丢失,复现率低,平均定位耗时 2.5 天。

第一步:现象与口径——把收益和代价分开记 ​

收益账:平均 TPS 从 12000 提升到 14640(+22%),CPU 利用率从 38% 升到 71%。

代价账:时序类故障每周 5–8 起,故障全部集中在多个线程并发改同一份世界状态的路径上;偶发、无固定复现步骤,平均定位 2.5 天。

对照口径:同期只读的寻路批处理与日志落盘没有出过问题——出问题的都是共享状态的读写。

第二步:分层归因——按本页状态隔离维度排查 ​

text
时序故障归因  占比
  共享世界状态跨线程自由流动,锁外读改写    62%
  顺序敏感逻辑被并行打散                     26%
  偶发时序 Bug 本身难以复现、排障成本高      12%

排查路径:故障集中在房间状态、Buff 结算、技能时序三处,都是「状态强耦合、需要顺序执行」的逻辑;而这些逻辑恰恰是本页说的多线程最不适合的对象——共享状态紧、顺序要求强、结果必须可重放。

第三步:根因——为性能过早并发化,把核心状态放进了最不易推理的模型 ​

改造只盯着 CPU 利用率,没有先问「这份状态能不能被多人同时改」「这条业务链路的执行顺序还稳不稳」。结果是把最怕不一致的核心状态,放进了时序可推理性最差的执行模型,锁竞争和时序问题一起放大。

第四步:处置与回填——核心状态回到单写,重计算外包 ​

  1. 房间与场景状态改回单写线程或 Actor 分区:每个房间一个执行单元,房间内顺序执行,状态不再跨线程共享。
  2. 重计算外包线程池:寻路批处理、日志落盘、外围统计这些弱耦合工作交给线程池,不回写核心状态。
  3. 明确顺序与并行边界:哪些部分必须顺序、哪些值得并行、哪些可异步但不得反向污染核心状态,写进架构评审。

复核账:时序类故障每周 5–8 起 → 0 起;TPS 保持 +18%(分区后略低于多线程峰值,但故障归零);同类问题平均定位耗时 2.5 天 → 0.5 天。

回填清单:

回填项动作回填处
状态隔离评估并发化改造前先评估状态能否被多人同时改、执行顺序是否还稳定为什么执行模型首先是状态问题
多线程只做弱耦合并行并行只用于重计算、异步 I/O、批处理等弱耦合工作多线程真正擅长什么
Actor 分区房间、场景、玩家实体按分区键落单写 Actor,房间内顺序执行Actor 到底解决了什么
混合式落点网关多线程 I/O、核心状态单写、重任务线程池,写进架构图一个更贴近游戏服务器的常见落点

案例的三个教训 ​

  1. 并发化前先问状态问题,不是先问 CPU。改造只盯利用率,把共享状态交给多线程,锁竞争和时序问题就会一起放大。
  2. 顺序敏感的核心逻辑不值得并行。房间战斗这类逻辑的价值在可推理、可回放,真并行换来的吞吐往往还不起排障成本。
  3. 混合式的关键在边界,不在混搭。哪些必须顺序、哪些值得并行写清楚,才谈得上「把最怕不一致的状态,留在最容易推理的执行模型里」。

Used By ​

执行模型选型;混合架构的责任划分。

  • Orleans(commit b1b557d):Virtual Actor 的「每激活单线程」有源码背书——per-activation 调度器 WorkItemGroup : IThreadPoolWorkItem, IWorkItemScheduler(src/Orleans.Runtime/Scheduler/WorkItemGroup.cs:17),注释「always called in a single-threaded environment」(同文件 :140);激活入口 DefaultGrainActivator(src/Orleans.Runtime/Activation/DefaultGrainActivator.cs)
  • KBEngine(commit 0bc93d5):单逻辑线程事件泵——主循环只跑 dispatcher(kbe/src/lib/server/serverapp.cpp:259),游戏 tick 内串行处理定时器与全部网络消息(kbe/src/lib/server/entity_app.h:698-712),线程池只承载离线与 DB 任务(kbe/src/lib/thread/、kbe/src/server/dbmgr/dbmgr.cpp:239-244)

关系链:状态问题 → 单线程生命力 → 多线程擅长 → Actor 解决什么 → 比较方式 → 混合式 → 常见落点。上游:runtime/00(立场总览)。下游:runtime/02(协程/状态机/队列的配合)、runtime/05(锁与单写者)。

Reference ​

本页为工程经验归纳;页内实战案例为教学示例(数字取整简化)。

游戏知识体系