C++用std::atomic::compare_exchange_weak实现非阻塞式原子互斥锁
compare_exchange_weak因允许伪失败而生成轻量级指令,适合自旋锁。配合循环重试可降低开销,需在循环中判断操作成功并重置expected,搭配acquire/release内存序。加锁时使用_mm_pause()避免过度消耗缓存一致性带宽。此锁为忙等待锁,仅适用于纳秒至微秒级短临界区。
compare_exchange_weak之所以适合自旋锁,关键在于它允许所谓“伪失败”,因此可以生成更轻量级的指令。配合循环重试机制,能够有效降低整体开销。使用时必须在循环中通过返回值判断操作是否成功,同时注意重置expected值,搭配acquire/release内存序即可满足需求。

先说说为什么 compare_exchange_weak 特别适合做自旋锁的底层操作。它在硬件层面映射为一条带条件的原子指令(比如 x86 下的 cmpxchg),失败时不会阻塞线程,只是返回 false。配合循环就能实现轻量级的自旋。相比 compare_exchange_strong,它更可能出现“伪失败”,但对自旋锁这种本来就要反复重试的场景来说,这反而更高效——CPU 不需要保证强一致性语义,省下了额外的内存屏障或重试开销。
用 std::atomic 实现最简自旋锁要注意什么
核心思路是把锁状态建模为一个 std::atomic,用 true 表示已加锁。这里容易踩的陷阱在于:必须用 memory_order_acquire 和 memory_order_release 来控制内存序,否则编译器或 CPU 可能重排临界区代码,导致数据竞争。
- 加锁循环中,
expected = false,反复调用lock_flag.compare_exchange_weak(expected, true, std::memory_order_acquire),失败后会自动更新 expected 为当前值,所以不用手动重置 - 解锁必须用
store(false, std::memory_order_release)——如果用错了memory_order_relaxed,临界区的写操作可能被重排到解锁之后 - 不能在构造函数里直接
store(false),要用ATOMIC_VAR_INIT或初始化列表,否则 C++17 之前可能触发未定义行为
compare_exchange_weak 循环里要不要 yield 或 pause
答案是“要”,但目的不是为了“让出 CPU 时间片”,而是避免在超线程或高争用场景下过度消耗前端总线和缓存一致性带宽。x86 上推荐用 _mm_pause()(需要 ),ARM 上可用 __builtin_arm_yield()。普通的 std::this_thread::yield() 开销太大,会陷入内核调度,反而违背了“非阻塞”的初衷。
示例代码片段如下:
while (!locked_.compare_exchange_weak(expected, true, std::memory_order_acquire)) {
expected = false;
_mm_pause(); // x86 only
}
这个锁真的“无锁”吗?常见误用点
它只是“不依赖操作系统锁原语”,但本质上仍然是忙等待锁(busy-waiting),并不是 lock-free 算法意义上的“无锁”(lock-free 要求至少有一个线程能持续进步)。容易忽略的几个问题:
- 析构前必须确保锁已释放,否则
~spinlock()销毁一个被持有的std::atomic本身是安全的,但从逻辑上看属于资源泄漏 - 不能递归加锁:同一个线程重复调用
lock()会导致死锁,因为compare_exchange_weak永远会失败 - 不适用于临界区较长的场景——CPU 会白忙一场,功耗和延迟都很难接受;它只适合纳秒到微秒级的操作
真正值得推敲的地方其实不在原子操作本身,而在于如何界定“短临界区”、怎样跟 RAII 机制(比如 std::lock_guard)结合,以及多核缓存一致性带来的实际性能抖动——这些不是靠一个 compare_exchange_weak 调用就能解决的。
