自旋锁仅适用于纳秒级临界区,执行时间应控制在几十纳秒至几微秒内,超过10μs即属误用;典型合规场景仅为单个变量原子读写或极简状态标记,涉及任何函数调用均应排除。

临界区必须短到什么程度才适合用自旋锁
临界区执行时间应控制在几十纳秒到几微秒内,超过 10μs 就该怀疑是否误用了自旋锁。Go 中典型合规场景仅限于对 int32、uint64 等单个变量的原子读写,或极简状态标记(如 isReady 布尔标志位)。
常见错误现象:在临界区内调用 time.Now()、访问 map、做字符串拼接、甚至调用空接口方法 —— 这些都远超自旋锁的安全窗口。
- 实操建议:用
runtime.nanotime()包裹临界区粗略打点,若平均耗时 > 5μs,立刻换sync.Mutex - 更稳妥的做法是:只要涉及任何函数调用(哪怕内置函数),默认不放进自旋锁保护范围
- Go 的
sync.Mutex本身在争用不激烈时会自动进入轻量级自旋(最多 30 次 CAS),你手动写的自旋锁往往并无优势,反而增加出错概率
为什么不能在单核或低配环境用自旋锁
单核 CPU 上,自旋锁会导致逻辑死锁:持有锁的 goroutine 被调度器切走后,等待者无限轮询,但没人能抢到时间片释放锁。即使多核,若系统负载高、GOMAXPROCS 设置过小,同样可能卡住。
错误信号:程序在压测时 CPU 占用率飙升但吞吐不增,pprof 显示大量 goroutine 停留在 runtime.gosched 或自旋循环里。
- 实操建议:生产环境禁用硬编码自旋锁;若真需,先检查
runtime.NumCPU()≥ 2 且runtime.GOMAXPROCS(0)≥ 2 - 避免依赖
runtime.Gosched()做退避 —— 它不保证让出给特定 goroutine,只是提示调度器可调度其他任务 - 真正安全的退避应结合指数增长 + 随机抖动,例如
for i := 0; i 后再 <code>backoff = min(backoff
atomic.CompareAndSwapUint32 之后要不要 memory barrier
要。Go 的 atomic 包操作默认提供顺序一致性(sequential consistency),但仅限于该原子变量自身。若临界区涉及多个变量(比如先改状态再写数据),必须用 atomic.StorePointer 或显式插入屏障,否则编译器/CPU 可能重排指令。
典型坑:只用 atomic.CompareAndSwapUint32(&flag, 0, 1) 成功后直接写共享结构体字段,其他 goroutine 可能读到 flag 已置位但结构体字段仍是旧值。
- 实操建议:临界区只操作单个原子变量;若必须多字段,用
sync/atomic提供的Store/Load组合,并确保所有读写路径使用相同内存序 - 简单方案:把多个字段打包进一个指针指向的结构体,用
atomic.StorePointer原子替换整个结构体指针 - 不要依赖
unsafe.Pointer强转绕过原子性 —— Go 1.20+ 对这类用法已加警告,且无法保证跨平台行为
sync.Mutex 已经够用,为什么还要手写自旋锁
几乎不需要。Go 标准库的 sync.Mutex 在低争用下会自旋约 30 次,且内部做了优化(如避免虚假唤醒、快速路径无锁化)。自己实现的自旋锁既难测试又易引入 ABA 问题、死锁或调度饥饿。
唯一合理场景:高频轮询的无锁队列节点状态(如 ants 中 worker 状态检查),且已确认该临界区稳定 ≤ 20ns、运行在稳定多核环境、并经过 pprof + trace 验证。
- 实操建议:先用
sync.Mutex实现,压测看mutex contention是否成为瓶颈;再用go tool trace确认锁等待集中在“短暂争用”而非“长持有” - 若真要替换,优先考虑
sync.Pool复用对象、减少共享,比锁优化收益更大 - 所有手写自旋锁必须带单元测试覆盖 timeout fallback 路径(比如自旋超 1000 次后强制 sleep)
go build -gcflags="-S" 看汇编,确认锁包裹的确实是那几条指令。











