swap 是无条件覆盖,cas 是有条件更新:仅当当前值等于预期旧值时才更新,因此 swap 适用于直接切换状态,而 cas 才是构建锁、计数器等无锁结构的基础。

Go 原子操作 Swap 和 CAS 的本质区别是什么
Swap 是无条件覆盖,CAS(CompareAndSwap)是有条件更新:只有当前值等于预期旧值时,才把值换成新值。这决定了它们的适用场景完全不同——Swap 适合“我不管现在啥样,直接换”,比如快速切换状态指针;CAS 才是构建锁、计数器、无锁队列的基石。
常见错误是拿 Swap 当 CAS 用,比如想“如果 flag 是 false 就设成 true”,结果用了 atomic.SwapUint32(&flag, 1),那就彻底绕过了条件检查,可能覆盖掉别人刚设的 true。
-
atomic.SwapUint32返回的是**旧值**,不是操作是否成功 -
atomic.CompareAndSwapUint32返回bool,必须检查返回值,否则等于没做条件判断 - 所有原子操作函数都要求变量地址对齐(
unsafe.Alignof检查),结构体字段若未对齐(比如uint32前面跟了个byte),直接传地址会 panic
用 CompareAndSwap 实现一个最简自旋锁
自旋锁的核心就是“不断尝试 CAS,直到成功”。它不挂起 goroutine,适合临界区极短(纳秒到微秒级)、竞争不激烈的场景。别把它当通用锁用——一旦临界区稍长或竞争激烈,CPU 就空转浪费,还可能饿死其他 goroutine。
下面是最小可行实现:
立即学习“go语言免费学习笔记(深入)”;
type SpinLock struct {
state uint32 // 0 = unlocked, 1 = locked
}
func (l *SpinLock) Lock() {
for !atomic.CompareAndSwapUint32(&l.state, 0, 1) {
runtime.Gosched() // 主动让出 P,避免独占 CPU
}
}
func (l *SpinLock) Unlock() {
atomic.StoreUint32(&l.state, 0)
}
- 必须用
runtime.Gosched(),否则在单 P 环境下会死循环,调度器没机会切走 -
Unlock用Store而非Swap或CAS,因为此时我们确定自己持有锁,不需要条件检查 - 这个锁不支持重入,也不记录 owner,两次
Lock()就卡死——这不是 bug,是设计选择
为什么不能直接用 Swap 实现自旋锁
有人试过:for atomic.SwapUint32(&l.state, 1) == 1 { },看起来像在等“换成功”,其实逻辑错了。因为 Swap 总会写入 1,无论原来是什么值——哪怕另一个 goroutine 刚释放锁(state=0),你一 Swap 就把它又设成 1,而那个刚释放的 goroutine 根本没机会执行后续代码。
更危险的是,如果多个 goroutine 同时 Swap,它们都拿到旧值 0 或 1,但谁也不知道自己是不是“第一个”抢到的,状态彻底失控。
-
Swap不提供原子性“读-判-写”语义,只提供原子性“写-返旧值” - 自旋锁依赖的正是“读当前值 → 判是否为 0 → 是则写 1”这一整套不可分割的动作
- 用
Swap模拟CAS需要额外内存屏障和重试逻辑,得不偿失,标准库不这么干
实际用自旋锁前必须确认的三件事
Go 官方几乎不在标准库中用自旋锁(sync.Mutex 底层在争抢激烈时会休眠,而不是自旋),不是因为不会写,而是因为绝大多数场景不适合。
- 临界区执行时间是否稳定且 int64 字段。超过 1μs 就该考虑互斥锁了
- 锁的争用是否极低?比如每秒最多几次冲突。高并发 ID 生成器这类场景可以,HTTP handler 里千万别用
- 你的程序是否运行在受限环境(如硬实时系统、协程密集型服务)?普通 Web 服务用
sync.Mutex更稳
最容易被忽略的是:自旋锁会让 P 忙等,如果 GOMAXPROCS=1,一个自旋就能卡住整个调度器。这不是理论风险,是真实会发生的事。










