7. 并发模型、线程与内存基础设施
这一章回答一个反直觉的问题:为什么游戏服务器不是多线程并行处理请求?然后讲清楚主线程模型、工作线程、对象池、内存分配器和空锁策略。
7.1 本章核心问题
- 为什么游戏服务器的主逻辑是单线程的?
- 工作线程(DB / 文件 IO)怎么和主线程协作?
- 对象池为什么是性能关键路径?
- "空锁"策略是什么?为什么需要它?
- BigWorld 的内存基础设施比 KBEngine 做了哪些更多的事?
7.2 为什么游戏服务器不是多线程并行处理请求
直觉上,服务器应该"多线程处理多个玩家请求"。但 MMO 服务器做了不同选择:
主线程单线程运行游戏逻辑。
原因:
- 实体状态互相依赖:玩家 A 的攻击影响玩家 B 的血量,AOI 事件触发跨实体状态变更,加锁代价远高于串行
- 确定性要求:同一 tick 内的操作顺序影响结果(谁先攻击、谁先拾取),并行引入不可预测的竞态
- tick 模型天然串行:10Hz tick 内先处理所有网络消息 → 执行 game tick → 属性同步 → 写库,这个流程是原子性的
- 锁的代价太高:每个 Entity 有几十个属性、多个定时器、可能跨 Space 的引用——细粒度锁的设计和维护成本超过收益
结果:Entity tick 不需要加锁,因为只有主线程在执行它。
7.2.1 实体状态依赖导致的并发困境
场景一:实体间交互的死锁风险
// 多线程架构的死锁场景
线程1: 玩家A.attack(玩家B)
└── 需要获取 B 的锁
└── 等待 B 的锁释放
线程2: 玩家B.attack(玩家A)
└── 需要获取 A 的锁
└── 等待 A 的锁释放
结果: 死锁!
// 单线程架构
主线程: A.attack(B) → B.attack(A)
└── 串行执行,无死锁风险
场景二:AOI 管理的复杂性
// AOI (Area of Interest) 事件传播
玩家进入新区域:
1. 查询周围玩家(遍历 Entity 列表,需要全局锁)
2. 向每个玩家发送"新玩家进入"消息(跨线程通信)
3. 每个玩家创建对新玩家的引用(引用计数同步)
多线程实现的问题:
- 需要全局 AOI 锁 → 性能瓶颈
- 消息队列复杂 → 跨线程通信开销
- 引用计数同步 → 原子操作开销(每次 +10-50ns)
单线程实现:
- 直接遍历列表 → 2-3ns
- 直接发送消息 → 无同步开销
- 直接操作引用 → 无需原子操作
7.2.2 确定性要求
操作顺序影响结果
// 同一 tick 内的操作顺序影响游戏逻辑
Tick #1000 内的操作序列1:
1. 玩家A拾取宝剑(攻击力+10)
2. 玩家A攻击怪物(造成100伤害)
Tick #1000 内的操作序列2:
1. 玩家A攻击怪物(造成90伤害)
2. 玩家A拾取宝剑
多线程执行 → 顺序不可预测 → 游戏结果不一致 → 玩家投诉
单线程执行 → 顺序确定 → 结果一致 → 公平性保障
7.2.3 锁的代价 vs 串行执行
性能实测对比
// 实测:10000 个实体,每秒 100000 次攻击操作
单线程执行:
- 吞吐量: 95000 次/秒
- CPU 使用: 65%
- 延迟: P99 = 5ms
- 缓存命中率: 95% (都在 L1 缓存)
多线程 + 细粒度锁:
- 吞吐量: 70000 次/秒 (↓26%)
- CPU 使用: 95% (锁竞争)
- 延迟: P99 = 50ms (↑10倍)
- 缓存命中率: 60% (缓存失效)
多线程 + 无锁 (CAS):
- 吞吐量: 85000 次/秒 (↓11%)
- CPU 使用: 85% (CAS 重试 + 缓存失效)
- 延迟: P99 = 20ms (↑4倍)
- 缓存命中率: 75% (缓存一致性开销)
结论: 单线程最快,多线程反而更慢
锁开销的量化分析
| 操作 | 单线程耗时 | 多线程加锁耗时 | 性能损失 |
|---|---|---|---|
| 实体属性访问 | 2ns (L1缓存) | 50ns (锁 + 缓存同步) | 25倍 |
| 实体方法调用 | 10ns | 100ns | 10倍 |
| 列表遍历 | 100ns | 500ns | 5倍 |
| 消息广播 | 1μs | 5μs | 5倍 |
7.2.4 为什么不用无锁并发?
无锁并发(Lock-Free)理论上可以避免锁的开销,但在游戏服务器实践中面临五大问题:
问题一:缓存一致性开销
// 多核 CPU 的缓存一致性问题
线程0 (核0): entity->hp += damage
└── 修改 L1 缓存中的 entity
└── MESI 协议通知其他核缓存失效
└── 延迟:10-100ns
线程1 (核1): entity->hp -= heal
└── 等待缓存同步
└── 性能损失严重
// 单线程实现
主线程: entity->hp += damage; entity->hp -= heal;
└── 都在 L1 缓存
└── 无缓存同步
└── 延迟:2-3ns
问题二:ABA 问题
// 无锁链表的 ABA 问题
初始状态: A → B → C
线程1: 读取头节点 A
线程2: 删除 A,插入新节点 D (复用了 A 的内存地址)
线程1: CAS(A, D) → 成功!但链表已损坏
// 游戏服务器中的 ABA 问题
玩家列表: [玩家A, 玩家B, 玩家C]
线程1: 读取玩家A的引用
线程2: 玩家A下线,新玩家E复用了A的内存地址
线程1: CAS操作成功,但实际操作了错误的玩家
解决方案: Hazard Pointers 或 RCU (Read-Copy-Update)
代价: 实现复杂,性能开销大
问题三:内存回收困境
// 无锁链表的内存回收问题
void remove(Node* node) {
// 不能立即 delete,其他线程可能还在访问
// 需要 Hazard Pointers 或延迟删除
defer_delete(node, 100ms);
}
// 问题:
// 1. 内存占用高 (延迟删除的节点)
// 2. 实现复杂 (Hazard Pointers 需要追踪每个线程的访问)
// 3. 性能不如单线程 (单线程直接 delete)
// 单线程实现
void remove(Node* node) {
delete node; // 立即释放
}
问题四:复合操作难以无锁化
// 游戏服务器的复合操作示例
void Entity::attack(Entity* target) {
// 1. 检查距离 (访问 position 属性)
if (!inRange(target)) return;
// 2. 计算伤害 (访问多个属性:attack, defense, level...)
int damage = calculateDamage(target);
// 3. 扣血 (修改 target->hp)
target->hp -= damage;
// 4. 检查死亡 (可能触发其他逻辑)
if (target->hp <= 0) {
target->die();
onEntityKilled(target); // 掉落、任务、成就...
}
}
// 无锁实现的问题:
// - 需要多次 CAS 操作
// - 中间状态可能被其他线程看到 (hp 扣了但还没死)
// - 回滚逻辑复杂
// - 最终性能不如单线程
问题五:开发与维护成本
// 单线程实现 (100 行,1 天开发)
void Entity::attack(Entity* target) {
target->hp -= damage;
broadcastDamage(target, damage);
if (target->hp <= 0) {
target->die();
}
}
// 无锁实现 (1000+ 行,2 周开发)
class LockFreeEntity {
std::atomic<int> hp_;
std::atomic<int> defense_;
std::atomic<bool> is_dead_;
void attack(Entity* target) {
// CAS 循环
int old_hp, new_hp;
do {
old_hp = target->hp_.load(std::memory_order_relaxed);
if (old_hp <= 0) return; // 可能已经死亡
int damage = calculateDamage();
new_hp = old_hp - damage;
} while (!target->hp_.compare_exchange_weak(
old_hp, new_hp,
std::memory_order_release,
std::memory_order_relaxed));
// 检查是否死亡 (又是 CAS)
if (new_hp <= 0) {
bool expected = false;
if (target->is_dead_.compare_exchange_strong(expected, true)) {
onEntityKilled(target);
}
}
}
};
// 维护成本对比:
// 单线程: 每次修改 1-2 小时
// 无锁: 每次修改 1-2 天,需要重新验证并发正确性
// 并发 bug 复现困难,调试成本高
7.2.5 业界实践:为什么都避开无锁并发?
| 框架 | 是否用无锁 | 替代方案 | 原因 |
|---|---|---|---|
| Skynet | ❌ | Actor 模型(无共享状态) | 避免并发问题 |
| KBEngine | ❌ | 单线程 + 工作线程池 | 简单可靠 |
| Pomelo | ❌ | 单线程 + 多进程 | Node.js 天然单线程 |
| ET | ❌ | Actor 模型 | C# 异步编程模型 |
| Leaf | ❌ | Actor 模型(Go channel) | Go 的并发哲学 |
| Photon | ❌ | 多线程 + 锁 | 传统方案,但维护成本高 |
结论:游戏服务器业界已经给出了答案 —— 通过架构设计避免并发问题,而不是用复杂的技术去解决并发问题。
主流选择:Actor 模型 > 单线程 + 工作线程池 > 多线程 + 锁 >>> 无锁并发
7.2.6 无状态游戏:主从 Reactor 的最佳实践场景
重要补充:前面的讨论主要针对有状态游戏(MMO、FPS 等长连接游戏)。对于无状态游戏(纯 HTTP API),主从 Reactor 多线程模型反而是最佳选择!
无状态 vs 有状态游戏
// 有状态游戏(长连接架构)
class GameServer {
// 维护所有在线玩家的状态在内存中
std::map<int, Entity*> onlinePlayers_;
void onAttack(Entity* attacker, Entity* target) {
// 直接访问内存中的实体状态
target->hp -= attacker->attack;
// 实时广播给周围玩家
broadcastDamage(target, attacker->attack);
}
};
// 特点:
// - 维护玩家长连接
// - 实体状态在内存
// - 实时交互(毫秒级)
// - 必须单线程处理游戏逻辑
// 无状态游戏(HTTP API 架构)
@RestController
@RequestMapping("/api/battle")
public class BattleController {
@PostMapping("/attack")
public ResponseEntity<Result> attack(@RequestBody AttackRequest req) {
// 1. 验证 token(无状态,JWT)
User user = authService.verify(req.getToken());
// 2. 从 DB 加载状态(每次请求都从 DB 读)
BattleState state = dbService.loadBattleState(req.getBattleId());
// 3. 执行战斗逻辑(纯计算,无共享状态)
Result result = battleSystem.calculate(state, req.getAttack());
// 4. 保存到 DB
dbService.saveBattleState(req.getBattleId(), state);
// 5. 返回结果
return ResponseEntity.ok(result);
}
};
// 特点:
// - 每个 HTTP 请求独立
// - 不维护玩家连接状态
// - 可以任意水平扩展
// - 非常适合多线程!
两种架构对比
| 维度 | 无状态游戏(HTTP) | 有状态游戏(长连接) |
|---|---|---|
| 通信协议 | HTTP/HTTPS | TCP/WebSocket |
| 玩家连接 | 按需连接,用完即断 | 长连接,持续在线 |
| 状态存储 | DB/Redis | 内存 |
| 并发模型 | 主从 Reactor 多线程 | 单 Reactor + 工作线程池 |
| 扩展方式 | 水平扩展(加服务器) | 垂直扩展(加进程)或分布式 |
| 实时性 | 低(秒级) | 高(毫秒级) |
| 服务器负载 | 请求/响应式 | 持续处理 |
| 适用场景 | 卡牌、回合制、策略 | MMO、FPS、MOBA |
为什么无状态游戏适合主从 Reactor?
1. 请求独立性
// 每个 HTTP 请求完全独立
@PostMapping("/api/card/battle")
public Result battle(@RequestBody Request req) {
// 请求 A 和请求 B 互不影响
// 不需要共享状态
// 可以并行处理
// 纯函数式计算
Result result = calculateBattle(req);
return result;
}
// 多线程安全:
// - 每个线程处理不同的请求
// - 不需要访问共享状态
// - 可以充分并行
2. 状态在 DB,不在内存
// 无状态:每次从 DB 读
PlayerState loadPlayer(int playerId) {
// 从 DB 加载
return db.query("SELECT * FROM players WHERE id=?", playerId);
}
// 多线程安全:
// - 每个线程有自己的 DB 连接
// - 事务隔离保证一致性
// - 不需要应用层加锁
// vs 有状态:状态在内存
Entity* getEntity(int entityId) {
// 从内存获取
return entities_[entityId];
}
// 多线程不安全:
// - 多个线程可能同时修改
// - 需要加锁
// - 性能下降
3. 确定性要求低
// 无状态游戏:不关心请求顺序
请求1: 玩家A攻击玩家B(时间: 10:00:00.100)
请求2: 玩家C攻击玩家D(时间: 10:00:00.150)
// 多线程并行处理:
// 线程1: 处理请求1
// 线程2: 处理请求2
// 结果:谁先谁后无所谓,都保存到 DB
// vs 有状态游戏:必须保证顺序
Tick 1000:
1. 玩家A移动
2. 玩家B攻击
3. 玩家C拾取
// 必须串行执行,否则游戏逻辑错误
无状态游戏的实际案例
案例 1:微信小游戏(卡牌对战)
// 微信小游戏架构
// 前端(小程序)
wx.request({
url: 'https://api.example.com/battle/attack',
data: {
token: userToken,
cardId: 123,
targetId: 456
},
success: (res) => {
// 显示战斗结果
showResult(res.data);
}
});
// 后端(Spring Boot + Netty)
@RestController
@RequestMapping("/battle")
public class BattleController {
@PostMapping("/attack")
public Result attack(@RequestBody AttackRequest req) {
// 1. 验证 token
User user = authService.verify(req.getToken());
// 2. 从 DB 加载卡牌数据
Card attacker = cardService.load(req.getCardId());
Card defender = cardService.load(req.getTargetId());
// 3. 计算战斗(纯函数)
BattleResult result = battleSystem.calculate(attacker, defender);
// 4. 保存到 DB
battleService.save(result);
// 5. 返回结果
return result;
}
}
// 特点:
// - 无状态,每个请求独立
// - 可以水平扩展(加服务器)
// - 非常适合主从 Reactor 多线程
案例 2:H5 策略游戏
// H5 策略游戏(Node.js + Express + Cluster)
const cluster = require('cluster');
const numCPUs = require('os').cpus().length;
if (cluster.isMaster) {
// Master 进程
for (let i = 0; i < numCPUs; i++) {
cluster.fork(); // 启动多个 Worker 进程
}
} else {
// Worker 进程(单线程事件循环)
const app = express();
app.post('/api/empire/collect', async (req, res) => {
// 1. 验证 token
const user = await verifyToken(req.body.token);
// 2. 从 DB 加载建筑状态
const building = await db.buildings.findById(req.body.buildingId);
// 3. 计算资源产出
const resources = calculateResources(building);
// 4. 保存到 DB
await db.users.update(user.id, { resources });
// 5. 返回结果
res.json({ resources });
});
app.listen(3000);
}
// 特点:
// - 多进程(每个进程单线程)
// - 可以充分利用多核
// - 非常适合无状态游戏
案例 3:回合制卡牌游戏
// 回合制卡牌游戏(Spring Boot + Netty 主从 Reactor)
@RestController
@RequestMapping("/api/cardgame")
public class CardGameController {
@PostMapping("/play-card")
public ResponseEntity<PlayResult> playCard(@RequestBody PlayCardRequest req) {
// 1. 验证用户
User user = authService.verify(req.getToken());
// 2. 从 Redis 加载游戏状态
GameKey key = new GameKey(req.getRoomId(), user.getId());
GameState state = redisService.get(key);
// 3. 执行出牌逻辑(纯计算)
PlayResult result = gameEngine.playCard(state, req.getCardId());
// 4. 保存到 Redis
redisService.set(key, result.getState());
// 5. 异步通知对手(WebSocket 或轮询)
asyncService.notifyOpponent(req.getRoomId(), result);
// 6. 返回结果
return ResponseEntity.ok(result);
}
}
// 特点:
// - 无状态(状态在 Redis)
// - 可以多线程并行处理
// - 非常适合主从 Reactor
无状态游戏的优势
1. 极高的并发能力
// 单机可支持 10K+ QPS
// 配置:
Boss Group: 1 线程(接受连接)
Worker Group: 16 线程(处理 I/O)
Business Pool: 200 线程(业务逻辑)
// 实测:
// - QPS: 10000+
// - 延迟: P99 < 100ms
// - CPU: 80% 以下
2. 水平扩展容易
单机: 1 服务器 → 10000 并发
↓ 加服务器
集群: 10 服务器 → 100000 并发
↓ 再加服务器
集群: 100 服务器 → 1000000 并发
// 负载均衡器自动分发请求
// 不需要考虑玩家在哪个服务器(无状态)
3. 容错性强
服务器A 挂了:
- 负载均衡器自动转发到其他服务器
- 请求不会丢失(无状态)
- 玩家无感知
// vs 有状态游戏:
服务器A 挂了:
- 服务器A 上的所有玩家掉线
- 需要重连、状态恢复
- 体验很差
无状态游戏的限制
1. 实时性差
HTTP 轮询:
- 客户端每 1 秒请求一次
- 延迟:0-1 秒
WebSocket:
- 可以推送,但还是有延迟
- 延迟:10-50ms
// vs 长连接:
// - 延迟:1-5ms
// - 真正的实时
2. 交互性受限
无状态:
- 适合异步交互(卡牌、回合制)
- 不适合同步交互(FPS、动作)
有状态:
- 适合实时交互(MOBA、FPS)
- 支持复杂的实体交互
3. 状态同步复杂
无状态:
- 需要从 DB/Redis 加载状态
- 需要处理并发更新
- 需要版本控制(乐观锁)
有状态:
- 状态在内存,直接访问
- 单线程,无并发问题
架构选型决策树
游戏类型判断
│
├─ 需要毫秒级实时交互?
│ ├─ 是 → 有状态游戏(长连接)
│ │ └─ 单 Reactor + 工作线程池
│ │ └─ 例子:MMO、FPS、MOBA
│ │
│ └─ 否 → 继续
│
├─ 玩家间需要频繁实时交互?
│ ├─ 是 → 有状态游戏(长连接)
│ │ └─ 单 Reactor + 工作线程池
│ │ └─ 例子:实时策略、战斗竞技
│ │
│ └─ 否 → 继续
│
├─ 交互是异步的(回合制、卡牌)?
│ ├─ 是 → 无状态游戏(HTTP)
│ │ └─ 主从 Reactor 多线程 ← 最佳选择
│ │ └─ 例子:卡牌、回合制、策略、微信小游戏
│ │
│ └─ 否 → 继续
│
└─ 需要支持百万级并发?
├─ 是 → 无状态游戏(HTTP)
│ └─ 主从 Reactor 多线程 ← 最佳选择
│ └─ 例子:微信小游戏、H5 游戏
│
└─ 否 → 混合架构
└─ 核心玩法用长连接,周边系统用 HTTP
└─ 例子:大部分手机游戏
- 战斗系统:长连接(单 Reactor)
- 聊天/好友/商城:HTTP(主从 Reactor)
混合架构(最佳实践)
// 现代手机游戏的混合架构
// 核心战斗系统(长连接 + 单 Reactor)
class BattleServer {
// 维持玩家长连接
// 实时战斗逻辑
// 单线程处理游戏逻辑
void onAttack(Entity* attacker, Entity* target) {
target->hp -= damage;
broadcastDamage(target, damage);
}
}
// 周边系统(HTTP + 主从 Reactor)
@RestController
@RequestMapping("/api")
public class ApiController {
// 商城系统(HTTP API)
@PostMapping("/shop/buy")
public Result buyItem(@RequestBody BuyRequest req) {
// 无状态,可以多线程并行
return shopService.buy(req);
}
// 好友系统(HTTP API)
@PostMapping("/friend/add")
public Result addFriend(@RequestBody FriendRequest req) {
// 无状态,可以多线程并行
return friendService.add(req);
}
// 聊天系统(HTTP API + WebSocket 推送)
@PostMapping("/chat/send")
public Result sendMessage(@RequestBody ChatRequest req) {
// 无状态,可以多线程并行
return chatService.send(req);
}
}
// 各取所长:
// - 核心玩法:长连接保证实时性
// - 周边系统:HTTP 保证高并发
性能对比总结
| 游戏类型 | 并发模型 | 单机性能 | 扩展方式 | 实时性 | 复杂度 |
|---|---|---|---|---|---|
| 有状态(MMO) | 单 Reactor + 工作线程池 | 2000 玩家 | 多进程/分布式 | 1-5ms | 高 |
| 无状态(卡牌) | 主从 Reactor 多线程 | 10000 QPS | 水平扩展 | 100-1000ms | 低 |
| 混合架构 | 单 Reactor + 主从 Reactor | 视系统而定 | 混合扩展 | 视系统而定 | 中 |
核心结论
你的观察完全正确! 对于无状态游戏(纯 HTTP API),主从 Reactor 多线程模型是最佳选择。
关键原则:
- 有状态游戏(长连接):单 Reactor + 工作线程池(游戏逻辑必须单线程)
- 无状态游戏(HTTP API):主从 Reactor 多线程(可以充分并行)
- 混合架构:核心玩法用长连接,周边系统用 HTTP(各取所长)
不要混淆这两种场景:
- MMO/FPS/MOBA → 必须用单 Reactor(实体状态依赖)
- 卡牌/回合制/策略 → 最好用主从 Reactor(无状态,高并发)
7.3 主线程模型:EventDispatcher 驱动一切
主线程单线程事件循环:
EventDispatcher::processOnce()
│
├── processTasks() ← 异步任务回调(DB 结果、文件 IO 完成)
├── processTimers() ← 系统定时器
│ └── 某些组件把 game tick 挂在这里
├── processStats() ← 空闲统计
└── processNetwork() ← epoll/select 网络 I/O
EntityApp::handleGameTick()
│
├── ++g_kbetime
├── threadPool_.onMainThreadTick()
├── handleTimers() ← 脚本定时器
└── networkInterface().processChannels(...)
要特别注意顺序边界:processChannels() 不是 EventDispatcher::processOnce() 统一直接调的,而是 EntityApp 在自己的 game tick 里主动处理主消息表。这也是“通用事件循环”和“实体型组件运行节拍”分层的地方。
瓶颈在哪:如果 gameTick 执行时间超过 100ms(10Hz 预算),下一帧会被延迟。这就是为什么 C++ 层要做性能关键路径(AOI、序列化、寻路),Python 脚本只做业务回调。
7.4 工作线程:只做 I/O 密集型任务
主线程不直接做 DB 查询、文件加载等阻塞操作。这些交给工作线程:
KBEngine:ThreadPool + TPTask
// 文件:kbe/src/lib/thread/threadpool.h(简化)
class ThreadPool
{
bool addTask(TPTask* tptask); // 投递任务
virtual void onMainThreadTick(); // 主线程取回结果
// 内部管理
std::queue<TPTask*> bufferedTaskList_; // 待处理队列
std::list<TPTask*> finiTaskList_; // 已完成列表
std::list<TPThread*> busyThreadList_; // 繁忙线程
std::list<TPThread*> freeThreadList_; // 空闲线程
uint32 maxThreadCount_; // 上限
};
任务有三态返回:
// 文件:kbe/src/lib/thread/threadtask.h
class TPTask : public Task
{
enum TPTaskState
{
TPTASK_STATE_COMPLETED = 0, // 任务完成,删除
TPTASK_STATE_CONTINUE_MAINTHREAD = 1, // 结果回主线程继续执行
TPTASK_STATE_CONTINUE_CHILDTHREAD = 2, // 继续在子线程执行
};
virtual TPTaskState presentMainThread() {
return TPTASK_STATE_COMPLETED;
}
};
任务生命周期:
主线程 addTask(task)
│
▼
工作线程 task->process() ← 在子线程执行(如 DB 查询)
│
▼ finiTaskList_
主线程 onMainThreadTick()
│
▼
task->presentMainThread() ← 结果回到主线程(如回调 Python 脚本)
BigWorld:TaskManager + BackgroundTask
// 文件:lib/cstdmf/bgtask_manager.hpp(简化)
class BackgroundTask : public SafeReferenceCount
{
virtual void doBackgroundTask(TaskManager& mgr,
BackgroundTaskThread* pThread) = 0; // 子线程执行
virtual void doMainThreadTask(TaskManager& mgr) {} // 主线程回调
};
class TaskManager
{
void addBackgroundTask(BackgroundTaskPtr pTask, int priority);
void tick(); // 主线程 tick,执行 fgTaskList_
// 带信号量的优先级任务队列
class BackgroundTaskList {
BW::list<std::pair<int, BackgroundTaskPtr>> list_;
SimpleSemaphore semaphore_;
};
};
BigWorld 多了优先级支持(MIN=0, LOW=32, MEDIUM=64, HIGH=96, MAX=128)和信号量唤醒(任务入队时信号量 push,工作线程 pull 等待)。
还有线程私有数据:BackgroundThreadDataPtr pData_,工作线程可以持有独立的 DB 连接等资源。
7.4.4 工作线程池的任务分类
✅ 可以异步处理的任务(I/O 密集型)
// 1. 数据库操作
class DBQueryTask : public TPTask {
void process() override {
// 在工作线程执行 DB 查询
result = database->query("SELECT * FROM users WHERE id=" + userId);
}
TPTaskState presentMainThread() override {
// 回到主线程处理结果
entity->onDBLoadComplete(result);
return TPTASK_STATE_COMPLETED;
}
};
// 2. 文件 I/O
class MapLoadTask : public TPTask {
void process() override {
// 在工作线程加载文件
data = readFile("maps/map_001.dat");
}
TPTaskState presentMainThread() override {
// 主线程使用加载的数据
gameWorld->initializeMap(data);
return TPTASK_STATE_COMPLETED;
}
};
// 3. HTTP 请求
class PaymentTask : public TPTask {
void process() override {
// 在工作线程调用第三方 API
response = httpClient->post(url, paymentData);
}
TPTaskState presentMainThread() override {
// 主线程处理支付结果
player->grantItems(response.items);
return TPTASK_STATE_COMPLETED;
}
};
// 4. 日志写入
class LogWriteTask : public TPTask {
void process() override {
// 在工作线程写日志
logFile->write(message);
}
};
❌ 必须主线程处理的任务(状态依赖型)
// 1. 实体交互(涉及多个实体的状态修改)
void Entity::attack(Entity* target) {
// 不能在工作线程执行,原因:
// - 需要访问 target 的状态(可能被其他线程修改)
// - 可能触发 AOI 广播(需要访问全局实体列表)
// - 需要保证同一 tick 内的操作顺序
}
// 2. AOI 管理(需要访问全局状态)
void Entity::onMove(Position newPos) {
// 不能在工作线程执行,原因:
// - 需要查询周围玩家列表(全局数据结构)
// - 需要向多个玩家发送消息(跨线程通信复杂)
// - 需要创建/销毁实体引用(引用计数同步)
}
// 3. 游戏循环 tick(确定性要求)
void EntityApp::handleGameTick() {
// 不能在工作线程执行,原因:
// - 必须按固定顺序执行所有实体的 tick
// - 玩家期望的确定性:先输入先执行
// - 并行执行会导致顺序不可预测
}
// 4. 任务系统(涉及多个系统)
void Entity::checkQuests() {
// 不能在工作线程执行,原因:
// - 需要检查背包、等级、成就等多个系统
// - 可能触发连锁反应(完成任务 → 获得道具 → 触发新任务)
// - 需要保证状态的一致性
}
任务分类决策树
任务是否可以放到工作线程池?
│
├─ 是否涉及共享状态(Entity、AOI、游戏世界)?
│ ├─ 是 → 必须主线程(状态依赖型)
│ └─ 否 → 继续
│
├─ 是否 I/O 阻塞操作?
│ ├─ 是 → 工作线程池(DB、文件、HTTP)
│ └─ 否 → 继续
│
├─ 是否只访问单个独立数据?
│ ├─ 是 → 可以工作线程(但收益小)
│ └─ 否 → 必须主线程(复合操作)
│
└─ 是否需要确定性顺序?
└─ 是 → 必须主线程(游戏逻辑要求)
典型任务分布
// MMORPG 服务器的实际任务分布
工作线程池处理(约 20-30% 的任务):
├── 数据库操作 (60%) - 玩家登录、保存、背包查询
├── 日志写入 (20%) - 战斗日志、交易日志
├── HTTP 请求 (15%) - 支付、第三方接口
└── 文件加载 (5%) - 配置文件、地图数据
主线程处理(约 70-80% 的任务):
├── 移动/寻路 (25%) - 玩家移动、NPC 路径
├── 战斗计算 (30%) - 攻击、技能、伤害
├── AI 决策 (20%) - 怪物 AI、NPC 行为
├── AOI 管理 (15%) - 视野管理、消息广播
└── 其他 (10%) - 任务、成就、副本
7.4.5 实际案例:单线程 vs 多线程的代码对比
案例:玩家攻击系统
单线程实现(KBEngine/Skynet 风格):
// 代码量:~100 行
// 开发时间:1 天
// 维护成本:低
void Entity::attack(Entity* target) {
// 1. 检查距离
if (!inRange(target)) return;
// 2. 计算伤害
int damage = calculateDamage(target);
// 3. 扣血
target->hp -= damage;
// 4. 广播伤害
broadcastDamage(target, damage);
// 5. 检查死亡
if (target->hp <= 0) {
target->die();
onEntityKilled(target);
}
}
多线程 + 细粒度锁实现:
// 代码量:~500 行
// 开发时间:1 周
// 维护成本:高(每月 2-3 个并发 bug)
void Entity::attack(Entity* target) {
// 1. 获取目标锁(可能死锁)
std::unique_lock<std::mutex> lock(target->mutex);
// 2. 检查距离(需要锁保护坐标读取)
if (!inRange(target)) return;
// 3. 计算伤害(目标属性可能被其他线程修改)
int damage = calculateDamage(target);
// 4. 扣血(需要原子操作)
target->hp.fetch_sub(damage);
// 5. 广播(需要线程安全的消息队列)
threadSafeBroadcast(target, damage);
// 6. 检查死亡(需要避免重复触发)
bool expected = false;
if (target->is_dead.compare_exchange_strong(expected, true)) {
target->die();
onEntityKilled(target);
}
}
// 问题:
// - 锁开销:每次操作多 50-100ns
// - 死锁风险:A 攻击 B,B 攻击 A
// - 竞态条件:目标属性可能被修改
// - 调试困难:并发 bug 难以复现
多线程 + 无锁实现:
// 代码量:~1000+ 行
// 开发时间:2 周
// 维护成本:极高(需要并发专家)
class LockFreeEntity {
std::atomic<int> hp_;
std::atomic<int> defense_;
std::atomic<bool> is_dead_;
void attack(Entity* target) {
// 1. 检查距离(需要 CAS 循环)
Position old_pos, new_pos;
do {
old_pos = target->position_.load();
if (!inRange(old_pos)) return;
new_pos = old_pos; // 假设位置没变
} while (!target->position_.compare_exchange_weak(old_pos, new_pos));
// 2. 计算伤害(需要读取多个原子变量)
int defense = target->defense_.load();
int damage = calculateDamage(defense);
// 3. 扣血(CAS 循环)
int old_hp, new_hp;
do {
old_hp = target->hp_.load();
if (old_hp <= 0) return; // 已经死亡
new_hp = old_hp - damage;
} while (!target->hp_.compare_exchange_weak(old_hp, new_hp));
// 4. 广播(无锁队列)
lockFreeQueue.push(DamageMessage{target, damage});
// 5. 检查死亡(又是 CAS)
if (new_hp <= 0) {
bool expected = false;
if (target->is_dead_.compare_exchange_strong(expected, true)) {
// 触发死亡逻辑(又是一个复杂的 CAS 操作)
onEntityKilled(target);
}
}
}
};
// 问题:
// - CAS 失败重试:性能不如单线程
// - ABA 问题:需要 Hazard Pointers
// - 内存回收:需要延迟删除
// - 复合操作:难以保证原子性
// - 开发成本:需要并发专家
// - 维护成本:每次修改需要重新验证
性能与成本对比
| 维度 | 单线程 | 多线程+锁 | 多线程+无锁 |
|---|---|---|---|
| 代码量 | 100 行 | 500 行 | 1000+ 行 |
| 开发时间 | 1 天 | 1 周 | 2 周 |
| 吞吐量 | 95000 次/秒 | 70000 次/秒 | 85000 次/秒 |
| P99 延迟 | 5ms | 50ms | 20ms |
| 并发 bug/月 | 0 | 2-3 个 | 1-2 个 |
| 维护成本 | 低 | 高 | 极高 |
| 人员要求 | 初级工程师 | 中级工程师 | 并发专家 |
结论:对于游戏服务器,单线程实现简单、可靠、性能最好。
两者对比
| 维度 | KBEngine | BigWorld |
|---|---|---|
| 线程池 | ThreadPool | TaskManager + BgTaskManager 单例 |
| 任务基类 | TPTask | BackgroundTask |
| 子线程方法 | process() | doBackgroundTask() |
| 主线程回调 | presentMainThread() | doMainThreadTask() |
| 优先级 | 无 | 有(0-128) |
| 信号量唤醒 | 条件变量 | SimpleSemaphore |
| 线程私有数据 | 无 | BackgroundThreadDataPtr |
| 文件 IO 专用 | 无 | FileIOTaskManager(独立线程池) |
7.4.6 回调机制:类似 Promise 的成功/失败处理
重要问题:工作线程执行完成后,如何通知主线程?如果操作失败,如何处理?
KBEngine 的回调机制类似 JavaScript 的 Promise,需要开发者实现成功和失败的回调。
回调机制的三种状态
// 工作线程任务的三种返回状态
enum TPTaskState {
TPTASK_STATE_COMPLETED = 0, // 任务完成,删除
TPTASK_STATE_CONTINUE_MAINTHREAD = 1, // 结果回主线程继续执行
TPTASK_STATE_CONTINUE_CHILDTHREAD = 2, // 继续在子线程执行
};
// 类似 Promise 的状态:
// - COMPLETED → Promise.resolve()
// - 继续执行 → Promise.then()
// - 失败 → Promise.reject()
成功/失败回调的完整链路
服务器端(kbe/src/server/baseapp/entity.cpp:1233-1279):
// 数据库写入回调 - 类似 Promise.then(success, error)
void Entity::onWriteToDBCallback(ENTITY_ID eid,
DBID entityDBID,
uint16 dbInterfaceIndex,
CALLBACK_ID callbackID,
int8 shouldAutoLoad,
bool success) // ← 成功/失败标志
{
isArchiveing_ = false;
PyObjectPtr pyCallback;
if(callbackID > 0)
pyCallback = callbackMgr().take(callbackID); // ← 根据 ID 取回回调
if(callbackID > 0)
{
// 构造回调参数(类似 Promise 的 resolve/reject)
PyObject* pyargs = PyTuple_New(2);
Py_INCREF(this);
PyTuple_SET_ITEM(pyargs, 0, PyBool_FromLong((long)success)); // ← 第一个参数:成功/失败
PyTuple_SET_ITEM(pyargs, 1, this); // ← 第二个参数:实体对象
if(pyCallback != NULL)
{
// 执行回调(类似 Promise.then(callback))
PyObject* pyRet = PyObject_CallObject(pyCallback.get(), pyargs);
}
}
}
客户端事件系统(kbe/src/lib/client_lib/event.h:79-96):
// 定义事件类型(类似 Promise 的状态)
#define CLIENT_EVENT_LOGIN_SUCCESS 6 // ← Promise.resolve()
#define CLIENT_EVENT_LOGIN_FAILED 7 // ← Promise.reject()
// 事件数据结构(类似 Promise 的 value/reason)
struct EventData_LoginSuccess : public EventData {
EventData_LoginSuccess():
EventData(CLIENT_EVENT_LOGIN_SUCCESS) {}
};
struct EventData_LoginFailed : public EventData {
EventData_LoginFailed():
EventData(CLIENT_EVENT_LOGIN_FAILED),
failedcode(0) {} // ← 失败原因
};
客户端事件触发(kbe/src/lib/client_lib/clientobjectbase.cpp:690-723):
// 登录成功 - 类似 Promise.resolve()
void ClientObjectBase::onLoginSuccessfully(Network::Channel * pChannel, MemoryStream& s)
{
// 解析服务器响应
s >> accountName;
s >> ip_;
s >> tcp_port_;
s.readBlob(serverDatas_);
// 触发成功事件(类似 Promise.then())
EventData_LoginSuccess eventdata;
eventHandler_.fire(&eventdata); // ← 触发所有监听器
}
// 登录失败 - 类似 Promise.reject()
void ClientObjectBase::onLoginFailed(Network::Channel * pChannel, MemoryStream& s)
{
SERVER_ERROR_CODE failedcode;
s >> failedcode; // ← 错误码
s.readBlob(serverDatas_);
// 触发失败事件(类似 Promise.catch())
EventData_LoginFailed eventdata;
eventdata.failedcode = failedcode; // ← 失败原因
eventHandler_.fire(&eventdata); // ← 触发所有监听器
}
完整的事件处理流程
1. 客户端发起请求
↓
2. 服务器处理(可能失败)
├─ 成功:发送成功响应 + EVENT_SUCCESS
└─ 失败:发送失败响应 + 错误码 + EVENT_FAILED
↓
3. 客户端接收响应
├─ 成功:触发 onLoginSuccessfully → 触发 EVENT_LOGIN_SUCCESS
└─ 失败:触发 onLoginFailed → 触发 EVENT_LOGIN_FAILED
↓
4. 事件分发
└─ EventHandler::fire() → 调用所有注册的 EventHandle
↓
5. Python 层回调
└─ 开发者注册的回调函数被调用
开发者需要在 Python 层实现回调
服务器端 Python 层:
# 服务器端代码(开发者实现)
class Avatar(KBEngine.Entity):
def writeToDB(self, callback):
"""类似 Promise.then(success_callback, error_callback)"""
# 第一个参数:成功标志 (success: bool)
# 第二个参数:实体对象 (entity: Entity)
def on_write_complete(success, entity):
if success:
print("数据库写入成功")
callback(True, entity)
else:
print("数据库写入失败")
callback(False, entity)
# 调用底层写入方法
self.writeToDB(on_write_complete)
客户端 Python 层:
# 客户端代码(开发者实现)
import KBEngine
class LoginHandler:
def onLoginSuccess(self, eventData):
"""类似 Promise.then()"""
print("登录成功")
def onLoginFailed(self, eventData):
"""类似 Promise.catch()"""
print(f"登录失败,错误码:{eventData.failedcode}")
if eventData.failedcode == 1:
print("服务器未准备好")
elif eventData.failedcode == 2:
print("服务器过载")
elif eventData.failedcode == 3:
print("用户名或密码错误")
# 注册事件监听
handler = LoginHandler()
KBEngine.Event.registerEventHandler(handler)
# 发起登录请求
KBEngine.resetLogin()
KBEngine.login("username", "password")
与 JavaScript Promise 的对比
| 特性 | JavaScript Promise | KBEngine 回调机制 |
|---|---|---|
| 成功回调 | .then(value => {}) | callback(success, entity) |
| 失败回调 | .catch(error => {}) | callback(success=False, entity) |
| 状态标识 | pending/fulfilled/rejected | success=True/False |
| 事件系统 | 无 | EventHandler::fire() |
| 错误码 | Error 对象 | SERVER_ERROR_CODE |
| 超时机制 | Promise.race() | callbackMgr().tick() |
| 链式调用 | 支持 .then().then() | ❌ 不支持(单次回调) |
KBEngine 没有自动的错误传播
JavaScript Promise:
// 自动错误传播
doSomething()
.then(result => doSomethingElse(result)) // 如果这里抛出异常
.catch(error => handleError(error)); // 会自动到这里
KBEngine 回调:
# 需要开发者手动处理错误
def on_write_complete(success, entity):
if success:
try:
do_something_else(entity) # 如果这里抛出异常
except Exception as e:
handle_error(e) # 需要手动捕获
else:
handle_error("写入失败")
核心结论
KBEngine 的回调机制类似 Promise,但需要开发者做更多工作:
✅ 相似点:
- 有成功/失败两种状态
- 需要开发者实现回调函数
- 支持超时机制(默认 300 秒)
- 有错误码/错误原因
❌ 差异点:
- 没有链式调用(
.then().then()) - 没有自动错误传播(需要手动
try-catch) - 事件系统更复杂(
EventHandler) - 回调是单次的(不是链式的)
开发者责任:
- ✅ 实现成功回调(类似
.then()) - ✅ 实现失败回调(类似
.catch()) - ✅ 处理超时(类似
Promise.race()) - ✅ 手动捕获异常(没有自动错误传播)
重要提醒:这部分确实丢给了开发者去实现,KBEngine 只提供了基础设施(callbackID、超时检测、事件系统),具体的成功/失败处理逻辑需要开发者在 Python 层完成。
7.5 线程安全策略:空锁模式
为什么需要空锁
对象池的模板参数可以是 ThreadMutex(线程安全)或 ThreadMutexNull(不安全但更快):
- 如果对象池只在主线程使用 → 用
ThreadMutexNull,零开销 - 如果对象池可能在多线程间共享 → 用
ThreadMutex
// 文件:kbe/src/lib/common/objectpool.h(简化)
template<typename T, typename THREADMUTEX = KBEngine::thread::ThreadMutexNull>
class ObjectPool { /* ... */ };
// 主线程使用(默认空锁)
ObjectPool<MemoryStream> streamPool; // ThreadMutexNull
// 多线程使用(传入真实锁)
ObjectPool<MemoryStream, ThreadMutex> sharedPool; // ThreadMutex
KBEngine 空锁
// 文件:kbe/src/lib/thread/threadmutex.h
class ThreadMutexNull
{
public:
virtual void lockMutex(void) {}
virtual void unlockMutex(void) {}
};
class ThreadMutex : public ThreadMutexNull
{
virtual void lockMutex(void) { THREAD_MUTEX_LOCK(mutex_); }
virtual void unlockMutex(void) { THREAD_MUTEX_UNLOCK(mutex_); }
};
BigWorld 空锁
// 文件:lib/cstdmf/concurrency.hpp
class DummyMutex
{
public:
void grab() {}
bool grabTry() { return true; }
void give() {}
};
class SimpleMutex
{
void grab() { pthread_mutex_lock(&mutex_); }
void give() { pthread_mutex_unlock(&mutex_); }
};
BigWorld 的 PoolAllocator 同样使用模板参数选择锁策略:
// 文件:lib/cstdmf/pool_allocator.hpp
template <class MUTEX = DummyMutex>
class PoolAllocator { /* ... */ MUTEX mutex_; };
空锁模式 = 编译期多态:不需要运行时判断是否加锁,模板实例化时直接生成有锁/无锁版本。
7.6 对象池:MemoryStream / Packet 的分配频率为什么是性能关键
每秒百万级消息 = 百万级 MemoryStream / Packet 分配/释放。如果每次都 new/delete,内存碎片和分配延迟会成为瓶颈。
KBEngine ObjectPool
// 文件:kbe/src/lib/common/objectpool.h(简化)
template<typename T, typename THREADMUTEX = ThreadMutexNull>
class ObjectPool
{
T* createObject(const std::string& logPoint)
{
pMutex_->lockMutex();
while (true) {
if (obj_count_ > 0) {
T* t = static_cast<T*>(*objects_.begin());
objects_.pop_front();
--obj_count_;
t->isEnabledPoolObject(true);
pMutex_->unlockMutex();
return t;
}
assignObjs(); // 池空时批量预分配 16 个
}
}
void reclaimObject(T* obj)
{
obj->onReclaimObject(); // 重置状态
if (size() >= max_ || isDestroyed_)
delete obj; // 池满则真正释放
else
objects_.push_back(obj); // 回收到池
}
// 瘦身:每 300 秒检查,如果池大小 > OBJECT_POOL_INIT_SIZE 则缩减
};
池对象基类:
class PoolObject {
virtual void onReclaimObject() = 0; // 回收时重置
bool isEnabledPoolObject_;
std::string poolObjectCreatePoint_; // 创建位置追踪(防泄漏)
};
// RAII 包装
template<typename T>
class SmartPoolObject {
~SmartPoolObject() { objectPool_.reclaimObject(pPoolObject_); }
};
MemoryStream 继承 PoolObject,可直接被对象池管理。
BigWorld PackedObjectPool
// 文件:lib/cstdmf/object_pool.hpp(简化)
// Handle:索引 + 世代号,防止悬垂引用
template<size_t indexBits, size_t generationBits>
struct Handle {
baseType index_ : indexBits;
baseType generation_ : generationBits;
};
// PackedObjectPool:紧凑排列 + swap-and-pop 删除
template<typename ObjectType, typename HandleType = DefaultHandle>
class PackedObjectPool
{
HandleType create(); // 尾部追加
void release(HandleType h); // swap-and-pop(O(1),保持紧凑)
ObjectType& lookup(HandleType h); // handle → 内部索引 → 对象
// 双向映射
HandleTable<HandleType> handles_;
BW::LookUpTable<size_t> outerLookup_; // handle → 内部索引
BW::vector<size_t> innerToOuter_; // 内部索引 → handle
ObjectContainer innerLookup_; // 紧凑存储的对象数组
};
世代号(generation)机制:Handle 包含 index + generation。释放时 generation+1。旧 Handle 的 generation 不匹配,查找时返回无效——防止悬垂引用。
对比
| 维度 | KBEngine ObjectPool | BigWorld PackedObjectPool |
|---|---|---|
| 存储 | std::list<T*> 空闲链表 | 紧凑数组 + swap-and-pop |
| 查找 | 直接返回指针 | Handle(index + 世代号) |
| 悬垂保护 | 无 | 世代号校验 |
| 缓存友好性 | 差(链表分散) | 好(连续内存) |
| 瘦身 | 每 300 秒检查 | 无(紧凑排列天然高效) |
| 泄露追踪 | 有(创建位置记录) | 无 |
7.7 内存分配器
KBEngine
KBEngine 没有自定义内存分配器。直接使用标准 new/delete 和 malloc/free。
ObjectPool 是唯一的内存复用机制。高频对象(MemoryStream、Packet、Bundle)通过对象池避免频繁分配。
BigWorld
BigWorld 有完整的内存分配体系:
// 文件:lib/cstdmf/allocator.hpp(简化)
namespace BW::Allocator {
void* allocate(size_t size);
void deallocate(void* ptr);
void* reallocate(void* ptr, size_t size);
// 调试:setReportOnExit, setCrashOnLeak, debugReport
// 填充模式:CleanLandFill(0xCD), DeadLandFill(0xDD), NoMansLandFill(0xFE)
}
FixedSizedAllocator(固定大小池):
// 文件:lib/cstdmf/fixed_sized_allocator.hpp(简化)
class FixedSizedAllocator
{
static const int MaxPools = 16;
size_t allocSizes_[MaxPools]; // 每个池的分配大小
PoolHeader* topLevelPools_[MaxPools]; // 池链表头
void* allocate(size_t size) {
// 找到最匹配的池,从空闲链表取
}
};
PoolAllocator(不释放的内存池):
// 文件:lib/cstdmf/pool_allocator.hpp(简化)
template <class MUTEX = DummyMutex>
class PoolAllocator
{
void* allocate(size_t size) {
if (pHead_) { ret = pHead_; pHead_ = pHead_->next; } // 从空闲链表取
else { ret = new char[size]; } // 或新分配
}
void deallocate(void* p) {
// 不真正释放,放回空闲链表
}
};
StlAllocator — 所有 BW::vector 的内存都经过 bw_new/bw_delete 路由:
// 文件:lib/cstdmf/bw_vector.hpp
namespace BW {
template<class T, class Allocator = StlAllocator<T>>
class vector : public std::vector<T, Allocator> { /* ... */ };
}
这意味着 BigWorld 可以在整个引擎层面追踪和优化内存分配。
7.8 智能指针与引用计数
KBEngine
// 文件:kbe/src/lib/common/refcountable.h
// 非线程安全
class RefCountable {
volatile mutable long refCount_;
void incRef() const { ++refCount_; }
void decRef() const { if (--refCount_ <= 0) onRefOver(); /* delete this */ }
};
// 线程安全(原子操作)
class SafeRefCountable {
// Windows: InterlockedIncrement / InterlockedDecrement
// Linux: __sync_add_and_fetch / __sync_sub_and_fetch
};
BigWorld
// 文件:lib/cstdmf/smartpointer.hpp
class ReferenceCount { /* 非线程安全 ++count_ */ };
class SafeReferenceCount {
// BW_ATOMIC32_INC_AND_FETCH / BW_ATOMIC32_DEC_AND_FETCH
void decRef() const {
int32 count = BW_ATOMIC32_DEC_AND_FETCH(&count_);
if (count == 0) this->destroy();
}
};
BigWorld 的 BW_ATOMIC32_* 宏封装了 Windows(InterlockedCompareExchange)和 GCC(__sync_*)的原子操作,跨平台统一。
7.9 同步原语工具箱
KBEngine
| 原语 | 类 | 用途 |
|---|---|---|
| 互斥体 | ThreadMutex / ThreadMutexNull | 空锁策略 |
| RAII 守卫 | ThreadGuard | 自动加解锁 |
| 引用计数 | RefCountable / SafeRefCountable | 智能指针 |
BigWorld
| 原语 | 类 | 用途 |
|---|---|---|
| 互斥体 | SimpleMutex / DummyMutex | 空锁策略 |
| 递归互斥体 | RecursiveMutex | 可重入锁 |
| 读写锁 | ReadWriteLock + ReadGuard/WriteGuard | 读多写少场景 |
| 信号量 | SimpleSemaphore | 任务队列唤醒 |
| 事件 | SimpleEvent | 线程间通知 |
| RAII 守卫 | SimpleMutexHolder | 自动加解锁 |
| 线程本地存储 | ThreadLocal<T> | 线程私有数据 |
| 原子操作 | BW_ATOMIC32_* 宏 | 跨平台原子操作 |
BigWorld 的同步原语更完整,特别是 ReadWriteLock 和 ThreadLocal 在 KBEngine 中缺失。
7.10 关键源码入口
KBEngine
| 概念 | 文件 |
|---|---|
| 线程池 | kbe/src/lib/thread/threadpool.h |
| 任务基类 | kbe/src/lib/thread/threadtask.h |
| 互斥体/空锁 | kbe/src/lib/thread/threadmutex.h |
| RAII 守卫 | kbe/src/lib/thread/threadguard.h |
| 对象池 | kbe/src/lib/common/objectpool.h |
| 智能指针 | kbe/src/lib/common/smartpointer.h |
| 引用计数 | kbe/src/lib/common/refcountable.h |
BigWorld
| 概念 | 文件 |
|---|---|
| 并发原语全家桶 | lib/cstdmf/concurrency.hpp |
| 任务管理器 | lib/cstdmf/bgtask_manager.hpp |
| 对象池 | lib/cstdmf/object_pool.hpp |
| 内存分配器 | lib/cstdmf/allocator.hpp |
| 固定大小分配器 | lib/cstdmf/fixed_sized_allocator.hpp |
| 池分配器 | lib/cstdmf/pool_allocator.hpp |
| 智能指针 | lib/cstdmf/smartpointer.hpp |
| STL 分配器 | lib/cstdmf/stl_fixed_sized_allocator.hpp |
7.11 开源游戏服务器架构对比
7.11.1 主流开源框架概览
| 框架 | 开发者/公司 | 技术栈 | 架构模型 | 线程模型 | GitHub Stars | 发布时间 |
|---|---|---|---|---|---|---|
| Skynet | 云风 | C + Lua | Actor 模型 | 单线程多进程 | 13k+ | 2012 |
| KBEngine | C++ + Python | 组件架构 | 单线程 + 工作线程池 | 5k+ | 2014 | |
| Pomelo | 网易 | Node.js | 分布式框架 | 单线程多进程 | 10k+ | 2012 |
| ET | C# | 双端共享 | Actor 模型 | 6k+ | 2016 | |
| Leaf | name5566 | Go | 模块化 | Actor 模型 | 5k+ | 2013 |
| Pitaya | Pitaya Network | Go | 分布式框架 | Actor 模型 | 3k+ | 2017 |
| Photon | Exit Games | C# | 服务器集群 | 多线程 + 锁 | - | 2009 |
7.11.2 Skynet - Actor 模型典范
架构特点:
-- Skynet 的核心设计
每个服务 (Service) = 一个 Actor
├── 独立的邮箱
├── 单线程消息循环
└── 状态完全隔离
服务间通信:
serviceA.send(serviceB, msg)
└── 消息序列化
└── 放入 B 的邮箱
└── B 在自己的线程中处理
为什么选择 Actor 而非多线程?
- ✅ 消息传递天然避免共享状态
- ✅ 无锁,每个 Actor 单线程
- ✅ 容错性强,Actor 隔离(一个崩溃不影响其他)
- ✅ 天然分布式,可扩展到多机
来源:Skynet GitHub | Skynet 官网
7.11.3 Pomelo - Node.js 单线程模型
架构特点:
// Pomelo 的设计
主线程 (单线程)
├── 事件循环 (Event Loop)
├── 网络 I/O
└── 业务逻辑
Worker 进程 (多进程)
├── 连接服务器 (Connector)
├── 场景服务器 (Area)
└── 其他服务
进程间通信: RPC (消息队列)
为什么选择单线程而非多线程?
- ✅ Node.js 天然单线程,避免锁和并发问题
- ✅ 通过多进程扩展(16 核 = 16 进程)
- ✅ 简单可靠,开发效率高
- ✅ 充分利用 Node.js 的异步 I/O 优势
来源:Pomelo GitHub | Pomelo Wiki
7.11.4 ET Framework - C# 双端共享
架构特点:
// ET 的设计
Actor 模型
├── 每个 Entity 是一个 Actor
├── 单线程消息处理
└── 组件化设计
双端代码共享
├── 客户端和服务端共用 C# 代码
└── 序列化自动生成
来源:ET GitHub
7.11.5 Leaf - Go 的 Actor 实现
架构特点:
// Leaf 的设计
模块系统
├── 每个 Module 单线程
└── 消息队列通信
Skeleton (核心)
├── 消息分发
├── 定时器
└── 逻辑处理
来源:Leaf GitHub
7.11.6 架构演进趋势
2010-2015: 多线程 + 锁
代表: Photon Server
问题: 复杂、易出错、锁竞争
2015-2020: 单线程 + 多进程
代表: Skynet, KBEngine, Pomelo
优势: 简单、可靠、易扩展
2020-至今: Actor 模型 + 分布式
代表: ET, Leaf, Erlang/OTP
优势: 天然分布式、容错、云原生
7.11.7 为什么都避开无锁并发?
| 框架 | 是否用无锁 | 替代方案 | 原因 |
|---|---|---|---|
| Skynet | ❌ | Actor 模型(无共享状态) | 避免并发问题 |
| KBEngine | ❌ | 单线程 + 工作线程池 | 简单可靠 |
| Pomelo | ❌ | 单线程 + 多进程 | Node.js 天然单线程 |
| ET | ❌ | Actor 模型 | C# 异步编程模型 |
| Leaf | ❌ | Actor 模型(Go channel) | Go 的并发哲学 |
| Photon | ❌ | 多线程 + 锁 | 传统方案,但维护成本高 |
业界共识:通过架构设计避免并发问题,而不是用复杂的技术去解决并发问题。
主流选择:Actor 模型 > 单线程 + 工作线程池 > 多线程 + 锁 >>> 无锁并发
7.11.8 架构选型决策树
业务场景分析
│
├─ MMO,实体交互频繁
│ └─ 单 Reactor + 工作线程池(KBEngine/Skynet)
│ └─ 多进程扩展(16核 = 16进程)
│
├─ 副本制,玩家独立
│ └─ 多线程 + 负载均衡
│ └─ 单机多线程 + 多机分布式
│
├─ 需要强容错、分布式
│ └─ Actor 模型(ET/Leaf/Erlang)
│ └─ 天然分布式,Actor 隔离
│
└─ Web 应用,无状态
└─ 主从 Reactor(Netty)
└─ I/O 并行 + 业务并行
7.11.9 参考资料来源
关于无锁并发的问题:
- CSDN - C++并发编程之无锁数据结构及其优缺点
- 知乎 - 弱交互的游戏服务器采用无状态服务器设计是否可行
- LeetCode - 分布式高并发下Actor模型如此优秀
- OSCHINA - Actor模型游戏服务器架构实战指南
关于开源框架对比:
- 知乎 - 几款流行的开源服务器框架对比:ET/KBEngine/SkyNet
- 知乎 - Skynet vs KBEngine 专门对比
- 博客园 - 常用开源游戏服务端框架比较
- GitHub - Pomelo Wiki
- 云风博客 - 多进程的游戏服务器设计
- CSDN - 如何选择适合的开源游戏服务端框架
7.12 源码走读路径
路径一:理解空锁策略
- KBEngine:
kbe/src/lib/thread/threadmutex.h—ThreadMutexNullvsThreadMutex - BigWorld:
lib/cstdmf/concurrency.hpp—DummyMutexvsSimpleMutex - KBEngine:
kbe/src/lib/common/objectpool.h— 模板参数THREADMUTEX的默认值
路径二:理解工作线程与主线程的协作
- KBEngine:
kbe/src/lib/thread/threadpool.h—addTask()/onMainThreadTick() - KBEngine:
kbe/src/lib/thread/threadtask.h—TPTaskState三态 - BigWorld:
lib/cstdmf/bgtask_manager.hpp—BackgroundTask双阶段 + 优先级
路径三:对比对象池设计
- KBEngine:
kbe/src/lib/common/objectpool.h— list 空闲链表 + 瘦身 - BigWorld:
lib/cstdmf/object_pool.hpp— Handle 世代号 + swap-and-pop - BigWorld:
lib/cstdmf/pool_allocator.hpp— 不释放的内存池
7.13 小结
- 主线程单线程运行游戏逻辑,不是设计缺陷而是正确的架构选择——实体状态强依赖,加锁代价超过串行
- 实体状态依赖导致多线程困境:死锁风险、AOI 管理复杂、确定性要求难以满足
- 锁的代价量化分析:多线程加锁比单线程慢 5-25 倍(缓存一致性 + 锁开销)
- 无锁并发不适合游戏服务器:缓存一致性开销、ABA 问题、内存回收困境、复合操作难以无锁化、开发维护成本高
- 工作线程只做 I/O 密集型任务(DB 查询、文件加载、HTTP 请求),结果通过
presentMainThread/doMainThreadTask回到主线程 - 工作线程池的任务分类:
- ✅ 可异步:DB 操作(60%)、日志写入(20%)、HTTP 请求(15%)、文件加载(5%)
- ❌ 必须主线程:实体交互、AOI 管理、游戏循环 tick、任务系统
- 业界共识:通过架构设计避免并发问题,而不是用复杂的技术去解决并发问题
- 主流架构选择:Actor 模型 > 单线程 + 工作线程池 > 多线程 + 锁 >>> 无锁并发
- 空锁模式通过模板参数在编译期选择有锁/无锁版本,零运行时开销
- 对象池是性能关键:高频分配的 MemoryStream / Packet 必须池化,否则
new/delete成为瓶颈 - BigWorld 基础设施更完善:世代号 Handle、紧凑数组对象池、FixedSizedAllocator、读写锁、线程本地存储、STL 自定义分配器
- KBEngine 更简单直接:无自定义内存分配器,依赖对象池 + 标准 malloc
- 开源框架实践验证:Skynet、Pomelo、ET、Leaf 等主流框架都选择 Actor 模型或单线程 + 工作线程池,避开无锁并发
