能,但必须用 std::atomic_flag_test_and_set_explicit 配合 acquire/release 内存序,自行实现忙等循环;初始化须用 ATOMIC_FLAG_INIT,不可默认构造或栈上反复析构,且仅适用于极短临界区。

std::atomic_flag 能不能当自旋锁用
能,但必须用 std::atomic_flag_test_and_set_explicit 配合 std::memory_order_acquire 和 std::memory_order_release,否则大概率出现数据竞争或死锁。它本身不提供“等待-重试”逻辑,所谓“忙等”得自己写循环。
常见错误是直接调 test_and_set() 不加内存序,或者在释放时用 clear() 却忘了指定 std::memory_order_release —— 这会导致其他线程看不到你改过的共享变量。
- 初始化必须用
ATOMIC_FLAG_INIT(C++17 起已弃用,但仍是唯一安全的零初始化方式) - 不能用
= {0}或默认构造初始化,否则值未定义 - 不要把它放在栈上反复构造/析构——
std::atomic_flag无拷贝、无移动,且非 trivially destructible
最简可用的忙等锁实现
下面这段代码能在所有主流编译器(GCC/Clang/MSVC)和 x86/ARM 上稳定工作:
struct spinlock {
std::atomic_flag flag = ATOMIC_FLAG_INIT;
void lock() {
while (flag.test_and_set(std::memory_order_acquire)) {
// 可选:__builtin_ia32_pause() 或 std::this_thread::yield()
}
}
void unlock() {
flag.clear(std::memory_order_release);
}
};
关键点不在“有没有 pause”,而在于 acquire/release 的配对:lock 用 acquire 确保后续读写不被重排到锁外,unlock 用 release 确保前面的写对其他线程可见。漏掉任一 memory order,就等于没锁。
立即学习“C++免费学习笔记(深入)”;
- 别用
std::memory_order_relaxed做锁操作——它不提供同步语义 - ARM/AArch64 下,acquire/release 会生成 dmb 指令;x86 天然强序,但依然要写明 order,否则标准不保证行为
- 如果锁内只操作局部变量,那这个锁其实没意义——它保护的是临界区里的共享数据访问
为什么不用 std::atomic 替代 atomic_flag
因为 std::atomic_flag 是唯一保证「无锁(lock-free)」的原子类型;std::atomic<bool></bool> 在某些平台(比如旧 ARMv7)可能退化为内部加互斥量实现,那就不是自旋锁了,而是偷偷调系统调用。
查是否真无锁,运行时用 flag.is_lock_free() 判断,但注意:这个函数返回 true 仅表示该对象实例无锁,不代表所有 std::atomic_flag 实例都一定无锁(虽然实践中几乎总是 true)。
-
std::atomic_flag大小固定为 1 字节,无对齐陷阱;std::atomic<bool></bool>可能因对齐要求占 4 字节,还可能触发 false sharing - 别试图用
std::atomic<int></int>的 compare_exchange 循环模拟——代码更长、易出错、且无法保证 lock-free - C++20 引入了
std::atomic_ref,但它不适用于栈上临时 flag,也不解决初始化问题
实际用在什么场景才合适
只适合极短临界区(比如更新几个指针、增减计数器)、且锁持有时间远小于系统调度开销的场景。一旦临界区里有 IO、内存分配、函数调用,或者可能被抢占超过几十微秒,忙等就变成耗电大户甚至卡死系统。
典型适用:用户态无锁队列的 head/tail 更新、RCU 的 grace period 标记、高性能网络栈中的 per-CPU 计数器。
- Linux 内核里 spinlock 在中断上下文必须忙等,但用户态程序没有这种约束,优先考虑
std::mutex - 调试时加
std::this_thread::yield()可缓解 CPU 占满,但 yield 不保证让出,也不能替代正确内存序 - 如果多个线程频繁争抢同一把 spinlock,缓存行在 CPU 核间反复同步,性能反而比 mutex 更差











