原子操作和互斥锁是解决竞态条件的两大核心工具:前者通过硬件指令保证单变量操作的不可分割性,轻量高效但仅限单一变量;后者通过内核调度保障临界区整体串行执行,适用复杂逻辑但有阻塞开销。

竞态条件不是靠“加个锁”或“换用原子”就能自动消失的——关键在于理解它们各自能管什么、不能管什么,以及在什么场景下会从“救星”变成“拖累”。
原子操作:快,但只管“单点”
原子操作(如 std::atomic、Interlocked.Increment、sync/atomic.AddInt64)本质是把“读-改-写”三步压成一条 CPU 硬件指令(比如 x86 的 CAS 或 LOCK XADD)。它不阻塞线程,没有上下文切换,所以极轻量。
- 适合场景:计数器增减、状态标志翻转、指针交换等单一变量的简单修改
- 典型陷阱:试图用原子操作保护多个变量(比如同时更新 count 和 last_updated),结果仍是竞态——原子性只作用于单个对象,不提供跨变量一致性
- 性能提示:低竞争时比锁快 10–20 倍;高竞争时可能因总线争抢导致退化,但依然远优于互斥锁的调度开销
互斥锁:慢,但能兜住“一整块逻辑”
锁(std::mutex、lock、sync.Mutex)不是靠硬件指令,而是靠操作系统内核协调线程调度。它保证的是“临界区”的串行执行——哪怕里面包含几十行代码、多个变量读写、甚至 I/O 调用。
- 适合场景:需要保持多变量间逻辑一致(如银行转账:扣A账户、加B账户)、操作复杂数据结构(如并发插入哈希表)、或涉及非原子外部调用
- 典型陷阱:锁粒度过粗(整个函数都上锁)、忘记解锁、嵌套锁顺序不一致引发死锁
- 性能提示:无竞争时现代实现(如 .NET 的自旋优化、C++ 的 futex 快路径)开销可控;但一旦出现争抢,线程阻塞+上下文切换会让耗时指数级上升——测试显示 100 线程累加时,锁比原子操作慢 21 倍
别只比快慢,先看“要不要同步”
很多性能问题其实源于根本不需要同步。例如只读共享数据(如配置缓存)、每个线程独占副本(thread-local storage)、或通过消息传递替代共享内存(actor 模型),都能绕开同步开销。
- 优先检查是否真有写冲突:如果变量只读不写,连 volatile 都未必需要
- 考虑无锁结构:如环形缓冲区、Michael-Scott 队列,它们用原子操作组合出线程安全行为,避免锁但比裸原子更强大
- 混合使用更现实:高频计数用原子,后台持久化用锁;或对哈希表分段加锁(sharded lock),降低争抢概率
验证比直觉更可靠
实际性能受 CPU 架构、缓存一致性协议(MESI)、内存顺序模型影响极大。同一段代码在 ARM 和 x86 上表现可能不同,加了 memory_order_relaxed 和 memory_order_seq_cst 的原子操作性能也差数倍。
- 用真实负载压测:模拟目标并发度和数据分布,别只跑“1 亿次自增”这种理想 benchmark
- 观察指标要具体:不只是总耗时,还要看线程阻塞时间、CPU 缓存未命中率、上下文切换次数(Linux 可用 perf stat)
- 工具辅助诊断:TSan(ThreadSanitizer)查竞态、Intel VTune 定位锁热点、Go 的 -race 标志捕获数据竞争











