自旋锁队列不是无锁队列,因其依赖忙等式加锁,违反lock-free“任意线程挂起时其余线程仍能完成操作”的定义;compare_exchange_weak在其中用于锁同步而非无锁推进,且易引发cache line失效与伪失败问题。

自旋锁队列不是无锁队列,compare_exchange_weak 用在自旋锁里不等于实现了 lock-free。
为什么自旋锁队列不能叫“无锁队列”
自旋锁本质仍是锁:一个线程持锁失败时不断重试 compare_exchange_weak,CPU 占着不放,其他线程只能等。一旦持有锁的线程被调度器挂起(比如被抢占、页错误、信号中断),所有等待线程就卡死——这违反 lock-free 的定义:「任意线程挂起时,其余线程仍能完成操作」。
常见误用场景:
- 把
std::atomic_flag+test_and_set()实现的自旋锁套在普通队列上,就标榜为“无锁” - 用
compare_exchange_weak更新 tail/head 指针,但没处理 ABA 或内存序,只在单生产者单消费者下跑通,就认为是多线程安全 - 把 boost::lockfree::queue 的接口误当成自旋锁封装,实际它底层是纯 CAS+内存屏障的 lock-free 链表或环形缓冲区
compare_exchange_weak 在队列指针更新中的典型用法
真正 lock-free 队列中,compare_exchange_weak 不是用来“加锁”,而是原子地推进指针并验证中间状态是否被其它线程篡改。以单生产者单消费者环形缓冲区为例:
入队时更新 tail:
size_t expected = tail.load(std::memory_order_acquire);
size_t desired;
do {
desired = (expected + 1) % capacity;
if (desired == head.load(std::memory_order_acquire)) {
return false; // 队满
}
} while (!tail.compare_exchange_weak(expected, desired, std::memory_order_acq_rel));
关键点:
-
expected必须是局部变量,每次循环都重读;compare_exchange_weak失败时会自动更新expected为当前值 - 必须配对使用
std::memory_order_acquire和std::memory_order_acq_rel,否则 head/tail 可见性无法保证 - 失败不等于错误——
compare_exchange_weak返回 false 时,只要重新计算desired并继续循环即可,这是它比strong更适合循环场景的原因
自旋锁队列 vs 真正无锁队列的性能分水岭
在高争用(比如 8+ 线程同时入队)下,自旋锁队列的 CPU 使用率会飙高,而真正的 lock-free 队列吞吐量更平稳。这不是因为自旋“快”,而是因为:
- 自旋锁下,所有竞争线程都在忙等同一个原子变量,cache line 频繁失效(false sharing),导致大量总线流量
- lock-free 队列(如 Michael-Scott 链表)把竞争分散到多个节点指针上,争用粒度更细
-
compare_exchange_weak在 ARM 等弱内存模型平台可能伪失败更频繁,但自旋锁对此毫无缓解能力;而 lock-free 队列的设计天然包容这种伪失败
如果你真需要高性能并发队列,直接用 boost::lockfree::queue 或 moodycamel::ConcurrentQueue,它们已处理了 ABA、内存序、padding 防 false sharing 等细节。自己手写 lock-free 队列,最容易翻车的不是 CAS 逻辑,而是忘记给 head/tail 加 std::atomic 修饰,或漏掉 memory_order 参数——C++ 默认是 std::memory_order_seq_cst,性能差且掩盖问题。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











