sync.mutex 不适合超低延迟场景,因其争用时触发 futex 系统调用进入内核态,上下文切换开销远超 100ns;自旋锁则全程用户态忙等,适合极短临界区。

为什么 sync.Mutex 不适合超低延迟场景
因为 sync.Mutex 在争用时会触发系统调用(如 futex),进入内核态,哪怕只等几纳秒,上下文切换开销也远超 100ns;而自旋锁全程在用户态忙等,适合临界区极短(
常见错误现象:goroutine 频繁阻塞在 Lock() 上,pprof 显示大量时间花在 runtime.futex 或调度器等待——这不是锁写得不好,是选错了锁类型。
- 仅当临界区是纯计算、无 I/O、无 channel 操作、无函数调用(或仅内联小函数)时才考虑自旋
- 必须配合
GOMAXPROCS≥ 实际物理核心数,否则自旋线程可能被抢占,反而更慢 - Go 1.19+ 的
runtime.Spinning状态可被 pprof 观察,但不会暴露给用户代码
手写 SpinLock 的最小安全实现
Go 没有原子布尔的“测试并置位”(TAS)内置指令封装,得靠 atomic.CompareAndSwapInt32 模拟;同时必须插入 runtime.Gosched() 防止单核死锁,并用 runtime.Pause()(Go 1.22+)或 runtime.nanosleep()(旧版)降低功耗。
type SpinLock struct {
state int32 // 0 = unlocked, 1 = locked
}
func (s *SpinLock) Lock() {
for !atomic.CompareAndSwapInt32(&s.state, 0, 1) {
runtime.Pause() // Go 1.22+, 否则用 runtime.nanosleep(1)
}
}
func (s *SpinLock) Unlock() {
atomic.StoreInt32(&s.state, 0)
}
- 不能用
atomic.AddInt32替代 CAS,会导致 ABA 问题和丢失唤醒 -
Unlock()必须用StoreInt32,不能用CAS,否则多 unlock 会 panic - 禁止在
Lock()中加time.Sleep或任何非内联系统调用,那已不是自旋锁
比手写更靠谱的选择:sync/atomic + 自定义逻辑
多数所谓“低延迟需求”,其实只是想避免 mutex 的调度开销,但又不敢承担纯自旋风险——这时应放弃封装成锁,直接用原子操作+业务逻辑合并临界区。
立即学习“go语言免费学习笔记(深入)”;
例如计数器累加:不用锁,改用 atomic.AddInt64(&counter, 1);状态机转换:用 atomic.CompareAndSwapUint32 做状态跃迁校验。
- 只要操作本身是原子的(读-改-写可单条指令完成),就根本不需要锁
- 多个字段需同步更新?把它们打包进一个
struct,用atomic.Value替代(注意分配逃逸) - 真要锁保护复杂结构,优先考虑
sync.RWMutex读优化,而非自旋
调试和压测时最容易忽略的三个点
自旋锁的问题往往在压测后期才暴露,且难以复现。
- 没有绑定 CPU 核心时,
runtime.Gosched()可能让 goroutine 迁移到其他 NUMA 节点,导致缓存行失效,实际延迟翻倍 - Go 的 GC STW 期间,自旋 goroutine 仍会空转,可能拖慢整个 STW,让 GC 时间变长(观察
gctrace中的STW字段) - 用
go tool trace查看Proc Status时,持续显示Running但无用户代码执行,大概率是自旋未退避或 Pause 失效
真正难的不是写对那十几行,是判断此刻该不该用它——临界区够不够短、负载是否稳定、机器有没有超线程干扰,这些都比锁本身更影响结果。










