compare_exchange_weak适合实现自旋锁,因其原子读-改-写语义天然匹配锁获取判断,x86上编译为单条cmpxchg指令,伪失败特性恰适循环重试;需在循环内重置expected并配合memory_order_acquire/release和_mm_pause优化。

compare_exchange_weak为什么能用来实现自旋锁
因为它的语义是“尝试用新值替换旧值,成功就返回true,失败就用当前实际值更新旧值”,这个原子读-改-写操作天然适合做锁的获取判断。它比 compare_exchange_strong 更轻量,尤其在x86上通常编译为单条 cmpxchg 指令,失败时也不保证重试一定成功,但正因如此,它更适合循环重试场景——自旋锁本来就要反复尝试。
怎么写一个最简可用的自旋锁
核心是用 std::atomic_flag 或 std::atomic<bool></bool> 控制状态,但若要用 compare_exchange_weak,推荐 std::atomic<int></int> 或 std::atomic<:uintptr_t></:uintptr_t> 表示锁状态(0=空闲,非0=已持有)。关键点在于循环中不断用期望值 0 去交换为当前线程标识(比如 tid 或 1):
class spinlock {
std::atomic<int> flag{0};
public:
void lock() {
int expected = 0;
while (!flag.compare_exchange_weak(expected, 1)) {
expected = 0; // 失败后重置期望值,因为 compare_exchange_weak 可能修改 expected 为当前实际值
// 可选:_mm_pause() 或 std::this_thread::yield() 减少忙等开销
}
}
void unlock() { flag.store(0, std::memory_order_release); }
};</int>
-
compare_exchange_weak第二个参数是 desired,不是“要加的值”,而是“想设成的值” - 必须在循环内重置
expected,否则下次调用时它可能仍是旧的实际值,导致逻辑错误 -
unlock()必须用std::memory_order_release,匹配lock()中的 acquire 语义(compare_exchange_weak默认是 acquire on success)
compare_exchange_weak在多核上容易踩的坑
它不保证“弱失败”一定是因并发冲突引起——某些平台(如 ARM、旧版 PowerPC)可能伪失败(spurious failure),即无竞争时也返回 false。这意味着你不能依赖“失败 == 锁被占”,而必须始终在循环里重试。另外,如果没加 _mm_pause() 这类提示指令,高争用下会显著增加总线流量和功耗:
- 不要写成
if (!flag.compare_exchange_weak(...)) { /* do something else */ }—— 它不是条件分支,是重试原语 - 不要省略循环体内的
expected = 0,否则第二次迭代传入的是上次读到的实际值,不再代表“期望空闲” - 在超线程或高负载系统中,裸循环可能饿死其他逻辑,生产环境建议限制最大重试次数后退避
和 std::atomic_flag 相比有什么实际差异
std::atomic_flag 是唯一保证无锁(lock-free)的原子类型,且接口极简(只有 test_and_set() 和 clear()),但语义固定为布尔切换,无法携带持有者信息;而用 compare_exchange_weak 配合 std::atomic<int></int> 虽然可能在某些平台退化为内部锁实现(可通过 is_lock_free() 检查),但它支持扩展,比如记录持锁线程ID用于死锁检测或调试。
真正复杂的地方不在写法,而在内存序选择和失败重试节奏——前者影响正确性,后者影响性能抖动。很多人调通了就以为没问题,但在线程调度突变或 NUMA 架构下,_mm_pause() 的有无、重试间隔的松紧,会直接决定锁争用时的尾延迟是否爆增。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











