自旋锁必须用memory_order_acquire和memory_order_release以建立同步点:加锁需看到之前所有写,解锁需确保本线程写对后续加锁者可见;仅用relaxed会导致重排、陈旧数据或写延迟,引发偶发竞争。

自旋锁为什么必须用 memory_order_acquire 和 memory_order_release
因为自旋锁的核心不是“锁住变量”,而是建立线程间访问的同步点:加锁要看到之前所有线程的写,解锁要确保自己的写对之后加锁的线程可见。只靠 memory_order_relaxed 会触发重排,导致临界区读到陈旧数据或写被延迟提交。
典型错误是把 compare_exchange_weak 全部设成 memory_order_relaxed,结果在 ARM/AArch64 或优化后的 x86 上出现偶发数据竞争——编译器和 CPU 都可能把临界区外的访存挪进锁内,或把锁内的写拖到锁外。
- 加锁成功时,要用
memory_order_acquire:它禁止后续读写上移(即保证“进入临界区后看到的都是新值”) - 解锁时,要用
memory_order_release:它禁止前面读写下移(即保证“临界区内所有修改在解锁后才对外可见”) - 失败重试的
compare_exchange_weak可以用memory_order_relaxed,但成功路径必须严格区分
std::atomic_flag 比 std::atomic 更适合实现自旋锁
std::atomic_flag 是 C++ 唯一保证无锁(lock-free)的原子类型,且默认初始化为 clear 状态,语义更贴近“锁”的本意;而 std::atomic 在某些平台可能退化为基于互斥量的模拟实现,反而引入隐藏锁开销。
常见误用是手动用 std::atomic + exchange(true) 实现 try_lock,但 exchange 默认是 memory_order_seq_cst,性能差,且无法精确控制 acquire/release 语义。
- 用
test_and_set(memory_order_acquire)实现加锁(注意:首次调用前需clear(memory_order_relaxed)初始化) - 用
clear(memory_order_release)实现解锁 - 不要对
std::atomic_flag使用load或store—— 它不提供这些接口,强行绕过会破坏语义
自旋锁不能直接替代 std::mutex,尤其在高争用或长临界区场景
自旋锁本质是“忙等”,CPU 一直执行空循环,不交出时间片。当锁被持有时长超过几个微秒,或者多个线程反复争抢,自旋消耗的 CPU 时间远超系统调度开销,反而降低整体吞吐。
典型踩坑是把数据库连接池的临界区、含系统调用(如 read/write)或内存分配(new)的代码塞进自旋锁——这些操作可能阻塞数十毫秒,期间其他核还在白跑。
- 适合场景:临界区极短(通常
- 不建议在用户态通用库中暴露裸自旋锁接口;若需,至少加上退避策略(如
std::this_thread::yield()或指数退避) - Clang/GCC 在 -O2 下可能将简单循环优化成
pause指令,但别依赖——显式调用_mm_pause()更可控(需)
一个最小可行的自旋锁实现要注意什么
不是写个 while(flag.test_and_set()) 就完事。真正能上线的版本必须处理初始化、内存序、退避和可调试性。
错误示例:flag.test_and_set() 没指定内存序,且没初始化;正确做法是用 ATOMIC_FLAG_INIT 或构造函数明确清零,并在加锁失败时插入退避。
- 初始化必须用
std::atomic_flag flag = ATOMIC_FLAG_INIT或 C++20 的std::atomic_flag flag{}; - 加锁循环里,失败后应调用
std::this_thread::yield(),避免单核全占;更优是用_mm_pause()(x86)或__builtin_ia32_pause()(跨平台) - 不要在析构函数里自动解锁——自旋锁不管理所有权,解锁必须由业务逻辑显式控制
- 调试时可加计数器统计自旋次数,但发布版务必删掉,避免干扰内存序和性能
复杂点在于:acquire-release 本身不保证操作原子性,只是约束重排;真正的原子性来自 test_and_set 的底层指令(如 x86 的 xchg)。这两层必须同时成立,缺一不可。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











