第2章 多线程

区分进程与线程,比较事件、临界区、互斥、信号量、互锁和可等待定时器,并用所有权与锁顺序消除竞态和死锁。

从第2章的核心问题开始

线程提高的是并发调度能力,不会自动提高正确性。游戏服务器应先决定状态由哪个线程拥有,再选择消息传递或最小同步原语。全局变量让共享变得容易,却把竞态、可见性和死锁扩散到所有路径。

官方目录命中与来源边界

本页明确覆盖:线程基础、进程、线程、线程的应用、Windows下的多线程处理、随机矩形、多工模拟程序、线程间的通信与同步、全局变量、事件、临界区、互斥、信号量、互锁访问、可等待定时器、线程死锁。这些名称来自 2007 年版公开书目。公开目录没有披露的细分内容会标为教学展开;现代 API、安全要求和部署方法用于帮助今天的读者验证原机制,不代表原书在 2007 年已经使用这些实现。

#机制或证据边界可观察解释
1进程与线程进程提供隔离的地址空间和故障边界;线程共享进程资源,切换成本更低,但任何未受控写入都可能破坏同一地址空间。
2应用不是越多越好网络接收、逻辑更新、数据库写入和日志可以并行,但线程数量必须由阻塞特征、核心数和队列背压决定。
3同步原语有不同语义事件表达状态通知,临界区保护进程内短临界段,互斥可跨进程,信号量限制并发额度,互锁适合单值原子更新,定时器触发计划工作。
4全局变量是风险放大器共享可变全局状态隐藏所有权。更稳妥的方案是单线程拥有状态,其他线程通过有界队列提交命令。
5死锁是等待图的环统一锁顺序、缩短临界区、禁止持锁阻塞 I/O,并在测试中制造反向获取,才能证明等待图不形成闭环。

核心概念与因果关系

、、、、。这些概念不能只停留在定义上:每个概念都要落到输入、内部状态、输出和失败证据。读者应能指出哪个模块拥有状态、哪个边界拒绝非法输入、哪个指标证明系统没有把成本推给下游。

进程与线程

进程提供隔离的地址空间和故障边界;线程共享进程资源,切换成本更低,但任何未受控写入都可能破坏同一地址空间。

验证时固定软件版本、配置和输入规模,先保存正常轨迹,再只改变一个边界条件。最终结果相同并不代表过程正确;必须比较阶段状态、队列水位、错误分类和恢复动作。

应用不是越多越好

网络接收、逻辑更新、数据库写入和日志可以并行,但线程数量必须由阻塞特征、核心数和队列背压决定。

验证时固定软件版本、配置和输入规模,先保存正常轨迹,再只改变一个边界条件。最终结果相同并不代表过程正确;必须比较阶段状态、队列水位、错误分类和恢复动作。

同步原语有不同语义

事件表达状态通知,临界区保护进程内短临界段,互斥可跨进程,信号量限制并发额度,互锁适合单值原子更新,定时器触发计划工作。

验证时固定软件版本、配置和输入规模,先保存正常轨迹,再只改变一个边界条件。最终结果相同并不代表过程正确;必须比较阶段状态、队列水位、错误分类和恢复动作。

全局变量是风险放大器

共享可变全局状态隐藏所有权。更稳妥的方案是单线程拥有状态,其他线程通过有界队列提交命令。

验证时固定软件版本、配置和输入规模,先保存正常轨迹,再只改变一个边界条件。最终结果相同并不代表过程正确;必须比较阶段状态、队列水位、错误分类和恢复动作。

死锁是等待图的环

统一锁顺序、缩短临界区、禁止持锁阻塞 I/O,并在测试中制造反向获取,才能证明等待图不形成闭环。

验证时固定软件版本、配置和输入规模,先保存正常轨迹,再只改变一个边界条件。最终结果相同并不代表过程正确;必须比较阶段状态、队列水位、错误分类和恢复动作。

六阶段证据链

可复现实验

让四个生产线程向一个逻辑线程提交玩家移动命令。先预测直接修改全局玩家表与有界队列两种方案的差异,再记录命令总数、最终状态、锁等待、队列高水位和关闭耗时。随后故意反向获取两把锁,验证看门狗能定位等待环。

最小实现

class CommandQueue {
 public:
  bool push(Command command) {
    std::unique_lock lock(mutex_);
    notFull_.wait(lock, [&] { return closed_ || queue_.size() < capacity_; });
    if (closed_) return false;
    queue_.push_back(std::move(command));
    notEmpty_.notify_one();
    return true;
  }
 private:
  std::mutex mutex_;
  std::condition_variable notEmpty_, notFull_;
  std::deque<Command> queue_;
};

边界处理

void transfer(Player& from, Player& to, int amount) {
  std::scoped_lock lock(from.walletMutex, to.walletMutex);
  if (from.gold < amount) throw InsufficientFunds{};
  from.gold -= amount;
  to.gold += amount;
}

证据契约

struct ConcurrencyEvidence {
  std::uint64_t acceptedCommands;
  std::uint64_t appliedCommands;
  std::size_t queueHighWatermark;
  std::chrono::microseconds maxLockWait;
  bool cleanShutdown;
};

实现代码只是实验的一部分。每次运行还要记录构建版本、配置摘要、样本类型、输入规模、首偏离节点、恢复动作和最终决策。若无法用相同输入重放,结论不能进入交付清单。

两个必须主动制造的失败

验收矩阵

维度正常样本边界样本失败样本通过条件
输入合法且固定容量、顺序或时序临界值畸形、重复或中断分类清晰且可重放
状态按六阶段推进达到预算但不越界在首偏离节点停止无隐式跨层写入
输出结果和证据一致退化可解释拒绝、补偿或回滚不变量恢复
交接他人按记录复验干净环境复验删除隐含依赖结论仍成立

本页签发不变量是:每个可变游戏状态只有一个写入所有者;关闭阶段不丢已确认命令;锁依赖图无环且队列达到上限时会施加背压。 任何演示、截图或单次成功都不能替代它。

练习

小结

  • 进程与线程:进程提供隔离的地址空间和故障边界;线程共享进程资源,切换成本更低,但任何未受控写入都可能破坏同一地址空间。
  • 应用不是越多越好:网络接收、逻辑更新、数据库写入和日志可以并行,但线程数量必须由阻塞特征、核心数和队列背压决定。
  • 同步原语有不同语义:事件表达状态通知,临界区保护进程内短临界段,互斥可跨进程,信号量限制并发额度,互锁适合单值原子更新,定时器触发计划工作。
  • 全局变量是风险放大器:共享可变全局状态隐藏所有权。更稳妥的方案是单线程拥有状态,其他线程通过有界队列提交命令。
  • 已用正常、边界和失败样本验证“每个可变游戏状态只有一个写入所有者;关闭阶段不丢已确认命令;锁依赖图无环且队列达到上限时会施加背压。”。
  • 已把公开目录、历史语境和现代教学展开分开记录。

术语表

来源与改编边界

讨论

评论区加载中…