Skip to content

并发模型:7种常见并发思路与适用场景

游戏服务器的并发模型决定了系统能承受多少连接、如何处理定时逻辑、以及开发复杂度。选择错误的并发模型会导致系统在高并发下崩溃,或者开发复杂度失控。

参考书籍:本章并发模型对比参考《百万在线》中的服务器架构选型实践,以及《游戏服务器架构与优化》中的并发模型章节。


1. 为什么并发模型如此重要

游戏服务器与 Web 服务器有一个本质区别:游戏服务器需要维护长时间的有状态连接。Web 请求是"无状态"的——每个请求独立处理,处理完就释放资源。但游戏连接是"有状态"的——一个玩家可能在线几小时甚至几天,期间服务端需要持续维护他的位置、背包、任务进度等状态。

这意味着:并发模型不仅要能处理高连接数,还要能高效管理大量长时间存在的状态。一个能处理 10 万连接但每个连接占用 1MB 内存的模型,需要 100GB 内存——这在实际中是不可接受的。

选择并发模型时需要权衡四个维度:连接容量(能处理多少连接)、开发复杂度(代码有多难写和维护)、延迟特性(每个操作的延迟是多少)、资源效率(每个连接占用多少内存和 CPU)。


2. 七种并发模型详解

2.1 单线程事件循环(Single-Thread Event Loop)

核心思路:一个线程跑一个无限循环,每次循环处理一批就绪的 I/O 事件和定时器,处理期间不阻塞。

典型代表:Node.js、Redis、Nginx worker

┌─────────────────────────────┐
│         Event Loop           │
│  ┌─────┐  ┌──────┐  ┌────┐ │
│  │ poll │→│ handle│→│ next│ │──→ 循环
│  └─────┘  └──────┘  └────┘ │
└─────────────────────────────┘

为什么它适合轻量级场景:单线程事件循环没有任何锁、没有竞态条件、代码逻辑非常清晰。对于小游戏服务器(如微信小游戏的后端)、网关层、轻量逻辑服务来说,它是常见选择。

为什么它不适合 MMO:单线程意味着所有连接的处理都在一个线程里。如果一个连接的处理耗时 10ms,其他 999 个连接都要等 10ms。在高并发下,延迟会急剧恶化。

适用场景:小游戏服务器、网关层、轻量逻辑服务、配置管理服务。

:Node.js 的事件循环中不能有同步阻塞操作(如 fs.readFileSync),否则会卡住整个循环。Go 中可以用 goroutine 模拟事件循环,但要注意 channel 的阻塞行为。

2.2 线程池(Thread Pool)

核心思路:预先创建一组工作线程,任务从共享队列中取出执行。I/O 密集时配合非阻塞 I/O。

典型代表:Java ExecutorService、C++ 自定义实现

为什么它适合计算密集型任务:线程池可以利用多核 CPU 并行处理任务。对于战斗数值计算、AI 寻路、地图 AOI 计算等 CPU 密集型任务,线程池是常见选择。

为什么它不适合高并发网络 I/O:每个线程占用约 1MB 栈空间,1000 个线程就要 1GB 内存。而且线程切换有开销(约 1-10 微秒),在高并发下会成为瓶颈。

适用场景:战斗数值计算、AI 寻路、地图 AOI 计算、日志写入。

:线程池中的任务如果访问共享数据,需要加锁。锁的竞争会导致性能下降——这是线程池最大的痛点。

2.3 Reactor 模型

核心思路:Reactor 监听 I/O 事件,分发给对应的 Handler 处理。Handler 在同一个线程中同步执行。

典型代表:Netty (Java)、libevent、libuv、Boost.Asio

┌──────────────────────────────────┐
│           Reactor                 │
│  ┌──────────┐    ┌────────────┐  │
│  │ demultiplex │→│ dispatch   │  │
│  │ (epoll)    │  │            │  │
│  └──────────┘    └────┬───────┘  │
│                       ↓          │
│  ┌────────┐ ┌────────┐ ┌────────┐│
│  │Handler1│ │Handler2│ │Handler3││
│  └────────┘ └────────┘ └────────┘│
└──────────────────────────────────┘

为什么它是网络代理层的首选:Reactor 模型用单线程处理网络 I/O,避免了线程切换和锁的开销。对于网关/代理层这种"转发消息、不做复杂计算"的场景,Reactor 是常见选择。

为什么它不适合做游戏逻辑:Handler 在 Reactor 线程中同步执行,如果一个 Handler 耗时较长,会阻塞后续所有事件的处理。游戏逻辑通常涉及数据库操作、复杂计算,不适合放在 Reactor 线程中。

适用场景:网关/代理层、高连接数低延迟服务、TCP 连接管理。

2.4 Proactor 模型

核心思路:发起异步 I/O 操作后,由操作系统在完成后通知应用(完成回调)。与 Reactor 的区别:Reactor 通知"就绪可读",Proactor 通知"读完了"。

典型代表:Windows IOCP、Linux io_uring、Boost.Asio (Proactor 模式)

为什么它比 Reactor 更高效:Reactor 模型中,Handler 需要自己调用 read() 来读取数据。如果数据还没到,read() 会阻塞(或者需要非阻塞 I/O + 重试)。Proactor 模型中,操作系统已经把数据读好了,Handler 直接处理数据即可——没有等待、没有重试。

为什么它更难实现:Proactor 依赖操作系统的异步 I/O 支持。Linux 的 io_uring 是一个很好的实现,但 API 复杂、调试困难。回调地狱(callback hell)也是 Proactor 模型的痛点。

适用场景:需要极致 I/O 吞吐的后端、跨服通信、大规模日志写入。

2.5 Actor 模型

核心思路:每个 Actor 是独立计算单元,拥有自己的状态和邮箱。Actor 之间只通过消息通信,不共享状态。天然隔离,无锁。

典型代表:Erlang/OTP、Akka (Java/Scala)、Microsoft Orleans

为什么它是 MMO 的理想选择:MMO 中每个玩家都是一个独立实体,有自己的状态(位置、背包、任务进度)。用 Actor 模型,每个玩家就是一个 Actor,所有操作都通过消息传递,天然避免了竞态条件。

为什么它不适合所有场景:Actor 之间通过消息通信,消息需要序列化/反序列化,这有 CPU 开销。而且 Actor 模型的调试比较困难——你需要追踪消息的流转路径,而不是像单线程那样直接看调用栈。

适用场景:MMO 实体管理、分布式游戏服务器(Erlang 风格)、聊天系统。

2.6 协程(Coroutine)

核心思路:用户态的轻量级线程,可被调度器在多个协程间切换。切换在用户态完成,无需内核介入,开销极低。

典型代表:Go goroutine、Kotlin 协程、Lua 协程、C++20 Coroutines、Erlang 进程

为什么 Go goroutine 是游戏服务器的首选:一个 goroutine 只占用约 2KB 内存,而一个 OS 线程占用约 1MB。这意味着你可以用同样的内存创建 500 倍数量的并发单元。而且 goroutine 的创建和切换开销极低(约 100 纳秒),远低于 OS 线程(约 1-10 微秒)。

为什么它不是银弹:大量 goroutine 会导致 GC 压力增大(需要扫描更多内存)。如果 goroutine 之间需要共享数据,仍然需要使用 sync.Mutex 等同步原语——协程只是简化了并发编程,没有消除并发问题。

适用场景:几乎所有 Go 游戏服务器都用 goroutine,每个连接一个协程、每个定时器一个协程。

2.7 CSP(Communicating Sequential Processes)

核心思路:进程之间不共享内存,只通过 Channel(带缓冲或无缓冲)通信。"Do not communicate by sharing memory; instead, share memory by communicating."

典型代表:Go channel、Occam、Ada

为什么它与 Go 天然契合:Go 语言的 channel 就是 CSP 模型的实现。在 Go 游戏服务器中,你可以把不同的系统(位置服务、战斗服务、聊天服务)设计为独立的 goroutine,通过 channel 通信。每个系统有自己的状态,不需要加锁。

为什么它比 Actor 模型更适合 Go:Actor 模型中,消息发送给特定的 Actor(需要知道 Actor 的地址)。CSP 模型中,消息发送给 Channel(发送者不知道谁接收)。这种解耦让系统更容易扩展——你可以随时添加新的消费者到同一个 channel。

适用场景:Go 游戏服务器架构首选,多系统解耦(位置服务、战斗服务、聊天服务各自独立进程)。


3. 七种模型对比

模型并发单元共享状态通信方式延迟吞吐开发难度游戏适用
单线程事件循环线程无竞态回调极低⭐⭐小游戏/网关
线程池线程需加锁共享队列⭐⭐⭐战斗/AI 计算
Reactor线程无竞态回调⭐⭐⭐网络代理层
Proactor线程无竞态完成回调极低极高⭐⭐⭐⭐I/O 密集后端
ActorActor无共享消息邮箱⭐⭐MMO/分布式
协程goroutine需同步channel极低极高Go 首选
CSP进程无共享channel⭐⭐多系统解耦

4. 实际选型建议

4.1 按语言选型

┌─────────────────────────────────────────────────────┐
│                   选型决策树                         │
├─────────────────────────────────────────────────────┤
│  语言是 Go?                                       │
│    └─ YES → goroutine + CSP(Go 天然支持)           │
│  语言是 Java?                                     │
│    ├─ 高并发网络 → Reactor (Netty)                  │
│    └─ 分布式实体 → Actor (Akka/Orleans)             │
│  语言是 C++?                                      │
│    └─ Reactor/Proactor (Boost.Asio / io_uring)     │
│  语言是 Lua?                                      │
│    └─ 线程池 + Lua 协程(Skynet 模式)              │
│  小型项目 / 快速原型?                              │
│    └─ 单线程事件循环 (Node.js)                      │
└─────────────────────────────────────────────────────┘

4.2 按游戏类型选型

游戏类型推荐模型原因
微信小游戏单线程事件循环连接数少、逻辑简单
卡牌/挂机单线程事件循环 + 协程无实时同步需求
MMOActor / CSP需要管理大量有状态实体
FPS/MOBAReactor + 协程需要低延迟网络 I/O
塔防协程需要定时逻辑,但不需要高并发
链游CSP多系统解耦,链上/链下分离

4.3 混合模型:现实中的常见做法

大多数成熟的游戏服务器不会只用一种模型,而是混合使用

  • 网关层:Reactor 模型(处理网络 I/O)
  • 游戏逻辑层:CSP/Actor 模型(处理业务逻辑)
  • 计算层:线程池(处理 AI、寻路等计算密集任务)
  • 定时器层:协程(处理心跳、buff 倒计时等定时逻辑)

这种分层架构让每层使用最适合的模型,既保证了性能,又控制了复杂度。


5. 常见陷阱与避坑指南

5.1 不要过早优化并发模型

很多团队在项目初期就花大量时间设计"完美的并发架构",结果发现实际需求根本用不上那么多并发能力。先用最简单的模型跑起来,等遇到性能瓶颈再优化——在很多项目中,这算是比较务实的策略。

5.2 不要忽视 GC 压力

Go 的 goroutine 虽然轻量,但大量 goroutine 会导致 GC 扫描的内存增多。在 Go 中,每个 goroutine 的栈空间会动态增长,但不会自动收缩。如果创建了 10 万个 goroutine,GC 需要扫描的内存可能达到几 GB。

解决方案:使用对象池复用 goroutine、限制同时存活的 goroutine 数量、定期触发 GC。

5.3 不要混淆"并发"和"并行"

并发是"同时处理多个任务"(可能在同一个线程上交替执行),并行是"同时执行多个任务"(需要多个线程)。单线程事件循环是并发但不并行;线程池是并发且并行。理解这个区别有助于你选择合理的模型。


6. 小结

关键问题答案
为什么需要并发?游戏服务器要同时处理数百到数万玩家连接和逻辑更新
Go 为什么适合游戏服务器?goroutine 极轻量(~2KB),channel 天然支持 CSP,一个连接一个 goroutine 成本极低
Actor 和 CSP 有什么区别?Actor 发消息给特定 Actor,CSP 通过 Channel 通信(发送者不知道谁接收)
Reactor 和 Proactor 区别?Reactor 通知"可读可写",Proactor 通知"操作已完成",Proactor 更高效但更复杂
Skynet 用什么模型?单线程 Actor(Lua 协程服务)+ 线程池

游戏后端知识体系