超短临界区才适合用std::atomic_flag手写自旋锁,因其不挂起线程而持续空转占用CPU;若临界区稍长(如超几十纳秒),自旋开销反超mutex上下文切换成本;仅当为纯内存操作、指令极少(如原子计数器增减)时才划算,且必须用ATOMIC_FLAG_INIT初始化,禁用阻塞调用,配合acquire/release内存序。

为什么超短临界区才适合用 std::atomic_flag 手写自旋锁
因为自旋锁不挂起线程,线程在等待时持续占用 CPU 执行空循环。如果临界区执行时间稍长(比如超过几十纳秒),自旋开销会远超系统互斥锁(如 std::mutex)的上下文切换成本,反而降低吞吐量。只有当临界区是纯内存操作、无分支、指令数极少(例如原子计数器增减、标志位设置)时,自旋才划算。
标准库没有提供裸自旋锁类型,std::atomic_flag 是唯一保证无锁(lock-free)且可移植的原子布尔类型,适合作为自旋锁底层原语。
- 避免用
std::atomic<bool></bool>实现自旋锁:它在某些平台可能被编译为基于锁的实现(如 x86 上虽通常无锁,但不保证) - 必须用
ATOMIC_FLAG_INIT初始化,否则行为未定义 - 临界区内禁止调用可能阻塞的函数(如
malloc、printf、任何带锁的 STL 容器操作)
如何用 std::atomic_flag 写一个最小可行自旋锁
核心逻辑就是反复尝试用 test_and_set() 占用标志位,直到返回 false(说明之前是清零状态,抢锁成功);释放锁则用 clear()。
struct spinlock {
std::atomic_flag flag = ATOMIC_FLAG_INIT;
void lock() noexcept {
while (flag.test_and_set(std::memory_order_acquire)) {
// 可选:插入 pause 指令降低功耗(x86)
// _mm_pause();
}
}
void unlock() noexcept {
flag.clear(std::memory_order_release);
}
};
-
std::memory_order_acquire保证 lock 后的读写不被重排到锁外 -
std::memory_order_release保证 unlock 前的读写不被重排到锁外 - 不要在
lock()中加std::this_thread::yield():这已不是纯自旋,且 yield 开销远大于空转,破坏设计初衷
哪些场景下 spinlock 会悄悄变慢甚至死锁
自旋锁对调度器和硬件极其敏感,常见失效点不在代码逻辑,而在运行环境。
立即学习“C++免费学习笔记(深入)”;
- 单核 CPU 或线程被固定到同一核:持有锁的线程若被调度器切走,等待线程将无限自旋 —— 这不是 bug,是设计限制
- 临界区里发生 page fault(如首次访问新分配内存):触发内核处理,持有锁线程长时间阻塞,其他线程全卡在自旋
- 编译器优化干扰:若临界区为空或只含常量操作,编译器可能优化掉整个循环体。务必确保临界区有实际副作用(如修改 volatile 变量、调用 non-inline 函数)
- ARM/AArch64 上缺少
sevl+wfe等等待指令支持时,纯test_and_set循环功耗极高,需手动加入轻量等待
比手写更稳妥的替代方案有哪些
除非你明确压测过且确认自旋锁带来可观收益,否则优先考虑更健壮的选项。
- 对单个整型计数器:直接用
std::atomic<int>::fetch_add()</int>,无需锁 —— 大多数超短临界区本质就是原子读-改-写 - 需要保护多字段结构体:用
std::atomic<std::pair<int, int>>(C++20 起支持 trivially copyable 类型的原子化)或std::atomic_ref(C++20)包装已有内存 - 仍需锁语义但想减少争用:用
std::shared_mutex(读多写少)或folly::MicroSpinLock(Facebook 工程验证过的生产级自旋锁,含退避与统计)
真正难的不是写出一个能跑的自旋锁,而是判断「此刻它是否比 std::mutex 更合适」—— 这需要看 perf record 的 cache-miss 热点、调度延迟直方图,而不是凭感觉。


















