go官方不提供sync.spinlock,因其调度模型下纯自旋会导致goroutine饿死;唯一可行起点是atomic.compareandswapuint32,需严格对齐、初始化为0,并配合runtime.gosched()或指数退避及超时fallback。

Go 官方不提供 sync.SpinLock,也不鼓励手动实现纯自旋锁——这不是功能缺失,而是调度模型决定的硬约束:goroutine 被抢占后继续自旋,只会饿死其他 goroutine,CPU 拉满却无实际进展。
为什么 atomic.CompareAndSwapUint32 是唯一可行起点
自旋逻辑必须基于原子比较交换,atomic.CompareAndSwapUint32 是 Go 唯一能保证“读-判-设”原子性的原语。用 atomic.StoreUint32 + atomic.LoadUint32 模拟锁状态会出竞态:两次原子操作之间存在时间窗口,多个 goroutine 可能同时判断为“空闲”并写入 1。
常见错误:
- 用
int32或bool当状态变量 →CompareAndSwapUint32编译不通过或 panic - 状态字段未对齐(如嵌套在 struct 中且前面有非 4 字节字段)→ 读写越界或原子性失效
- 初始化值不是 0 → 第一次
CAS总失败,直接进入无限循环
runtime.Gosched() 不是可选,是保命措施
裸写 for !CAS { } 等价于把一个 P 绑死在一个 goroutine 上,其他 goroutine 永远得不到调度。哪怕只加一行 runtime.Gosched(),也能让出当前 M 的执行权,给调度器机会安排别的 G 运行。
更稳妥的做法是指数退避:
- 首次失败后调用
runtime.Gosched() - 连续失败时,逐步增加
time.Sleep(1 * time.Nanosecond)延迟(比如 1ns、2ns、4ns…) - 退避上限建议设为
16次,再往后大概率说明临界区过长或争用过高,该 fallback 到阻塞锁
超时 fallback 是硬性要求,不是优化项
自旋必须带计数或时间限制,否则一旦临界区因 panic、死循环或意外阻塞未释放锁,所有等待 goroutine 就永久卡在自旋里。CPU 占用 100%,但业务完全停滞。
实操建议:
- 最多自旋 1000 次,之后直接调用
mu.Lock()(mu是预埋的sync.Mutex) - 不要尝试在自旋锁里做任何可能 panic 的操作(如解引用 nil 指针、切片越界)
- 解锁必须用
atomic.StoreUint32(&s.state, 0),不能用CAS回写 —— 如果临界区提前 return 或 panic,CAS失败会导致锁永远无法释放
真正适合自旋锁的场景极少:临界区稳定在 50ns 内、单次原子操作(如计数器增减)、调用方绝对不嵌套、且锁争用概率低于 0.1%。多数时候,sync.Mutex 内置的短时自旋(约 4 次 CAS 尝试)已足够,手动实现反而增加失控风险。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











