Q78: 如何处理竞态条件?
核心结论
竞态条件的本质不是“线程多了就会乱”,而是多个执行路径对同一份状态的读写顺序不再可控。
最有效的处理方式通常不是事后补锁,而是先减少共享状态、明确状态所有权,并让关键修改具备原子边界。
一、竞态条件真正是什么
只要程序结果依赖于执行先后顺序,而这个顺序又不受控制,就可能出现竞态。
它不只发生在:
- 多线程
也可能发生在:
- 异步回调
- 定时器与主流程交错
- 跨服务重复请求
所以竞态是“状态时序问题”,不只是“线程问题”。
二、最常见的竞态来源
例如:
- 检查后再修改,中间状态已变化
- 同一对象被多个线程同时写
- 超时重试和原请求并发到达
- 回调结果比对象生命周期更晚返回
这些比课本上的 counter++ 更接近真实线上问题。
三、最有效的第一步通常是减少共享
如果一份状态可以明确归属于:
- 某个线程
- 某个 Actor
- 某个会话或房间
那很多竞态就会自然消失。
这通常比在共享对象上不断补锁更稳。
四、原子边界必须明确
很多竞态来自:
- 读出旧值
- 计算新值
- 写回时已经过期
所以关键更新通常需要:
- 原子操作
- 锁保护
- 版本号
- compare-and-swap
具体选什么,取决于共享模式和复杂度。
五、检查与使用之间最容易出问题
这是实际工程里非常典型的一类竞态:
- 先判断“对象存在”
- 下一行使用时对象已经被销毁或转移
所以很多时候真正需要的是:
- 生命周期绑定
- 版本校验
- 持有期内的稳定引用
而不只是再加一把锁。
六、异步系统里的竞态往往更隐蔽
例如:
- RPC 超时后又晚回
- 定时器取消后回调仍可能到达
- 玩家下线后数据库异步保存才回来
这类问题常见处理方式包括:
- 请求 ID
- 版本号
- 状态机校验
- 失效标记
七、工程上更稳妥的处理方式
常见做法是:
- 状态尽量单拥有者
- 多步修改压成原子边界
- 异步回调带上下文 ID 或版本
- 关键对象生命周期显式管理
这样通常比“哪里出问题就哪里补锁”更稳。
八、常见误区
1. 竞态条件就是数据竞争
数据竞争只是其中一种。很多业务竞态发生在异步链路和状态机之间。
2. 加锁就能解决所有竞态
不一定。生命周期、重复请求和异步回调问题,很多时候需要状态机和版本控制。
3. 只有并发高才会有竞态
低并发下也可能发生,只是更难复现。
参考资料
- 竞态条件、版本控制与异步状态机实践资料
