
自旋锁该不该自适应?先看你的场景
绝大多数 C++ 项目不需要自己写自旋锁——std::atomic_flag 配合 test_and_set 就够用,std::mutex 在争用低时也基本不进内核。只有在极少数场景下才值得引入“自适应”逻辑:比如高频短临界区(
怎么实现“自适应”?核心是控制 pause 和退避策略
自适应的关键不是“猜”,而是基于最近几次尝试结果动态调整等待强度。x86 上必须用 _mm_pause()(或 __builtin_ia32_pause),它比空循环省电、减少乱序执行干扰;ARM 上对应 __builtin_arm_yield。但别直接裸用——要叠加指数退避:
- 首次失败:只调用 1 次
_mm_pause() - 第二次失败:循环调用 4 次
_mm_pause() - 第三次失败:休眠 1 微秒(
std::this_thread::sleep_for(1us)) - 超过 5 次失败:直接挂起线程(
std::this_thread::yield()或转入std::mutex回退路径)
注意:_mm_pause() 在虚拟机里可能被忽略,且对超线程敏感——同一物理核上两个逻辑核同时自旋,会互相拖慢。
std::atomic_flag 的 test_and_set 调用方式决定是否可自适应
自适应的前提是你能反复尝试而不破坏原子性状态。必须用带 memory_order_acquire 的轮询:
立即学习“C++免费学习笔记(深入)”;
while (flag.test_and_set(std::memory_order_acquire)) {
// 自适应等待逻辑放这里
}
不能用 memory_order_relaxed——它无法防止编译器/CPU 重排导致的临界区污染;也不能用 memory_order_release 做获取操作——语义错误。更隐蔽的坑是:某些老版本 libstdc++ 对 test_and_set 的 acquire 实现不完整,建议在 GCC 11+ / Clang 14+ 下验证。
为什么不用 std::shared_mutex 或 folly::MicroSpinLock?
std::shared_mutex 是重量级锁,内部有系统调用路径,根本不算自旋锁;folly::MicroSpinLock 确实自适应,但它默认启用 pthread_yield 回退,在容器密集场景下容易触发 futex_wait,反而放大抖动。自己写的轻量版优势在于可控:你可以把退避阈值硬编码成常量(比如 constexpr int MAX_SPIN_COUNT = 32;),避免运行时查表或计数器带来的 cacheline 伪共享。
真正难的是边界判断——比如 NUMA 节点跨访问时,即使只自旋 10 次,延迟也可能跳到 500ns 以上。这时候“自适应”得感知拓扑,而标准库不提供接口。这事没法全自动,得靠 perf + lscpu 配合压测定死参数。











