Table of contents
Open Table of contents
TL;DR
CAS 既是乐观锁也是无锁编程,不矛盾——“乐观锁”描述并发策略,“无锁”描述实现方式(不用 mutex)。比 CAS 更激进的方向是 FAA(一次成功)→ 分片(消除共享)→ RCU(读者零开销)。
CAS 是乐观锁也是无锁编程
这两个标签看的角度不同:
| 术语 | 描述的维度 | 含义 |
|---|---|---|
| 乐观锁 | 并发策略 | 假设不冲突,冲突了重试 |
| 无锁编程(lock-free) | 实现方式 | 不用 mutex,不阻塞等待 |
| CAS | 具体原语 | Compare-And-Swap,CPU 提供的原子指令 |
// CAS 循环:乐观锁的行为 + 无锁的实现
for {
old := atomic.LoadInt64(&balance)
new := old - 50
if atomic.CompareAndSwapInt64(&balance, old, new) {
break // 成功
}
// 被别人改了,重试 ← 乐观锁行为
// 但没有 mutex,没有阻塞等待 ← 无锁实现
}
“无锁”不是”没有并发控制”,而是没有 mutex、没有阻塞等待。CAS 失败了是 spin(重试),不是挂起等唤醒。
激进程度层次
mutex(悲观锁,阻塞等待)
↓ 不阻塞
CAS 循环(lock-free,可能重试)
↓ 不重试
FAA / atomic.Add(wait-free,一次成功)
↓ 不竞争
分片 / per-CPU(消除共享)
↓ 读者零开销
RCU(读者完全无感知)
1. CAS(Lock-free)— 可能重试
for {
old := atomic.LoadInt64(&counter)
if atomic.CompareAndSwapInt64(&counter, old, old+1) {
break
}
}
保证:至少一个 goroutine 在有限步内成功(lock-free)。但个别 goroutine 可能一直被抢占,理论上无限重试。
高竞争下 CAS 自旋浪费 CPU,这是它的天花板。
2. FAA / atomic.Add(Wait-free)— 一次成功
atomic.AddInt64(&counter, 1) // 一条硬件指令,没有失败的可能
保证:每个 goroutine 都在有限步内完成(wait-free),比 lock-free 更强。
局限:只能做加减,不能做”先判断再修改”的条件操作。
3. 分片(消除共享)— 根本不竞争
type Counter struct {
shards [8]atomic.Int64 // 按 goroutine 分片
}
func (c *Counter) Inc(id int) {
c.shards[id%8].Add(1) // 各写各的,零竞争
}
func (c *Counter) Total() int64 {
var sum int64
for i := range c.shards {
sum += c.shards[i].Load()
}
return sum
}
Linux 内核的 per-CPU 计数器、Go sync.Pool 内部的 per-P 分片都是这个思路。
代价:读取总值需要聚合所有分片,有一致性延迟。适合写多读少的计数场景。
4. RCU(Read-Copy-Update)— 读者零开销
Linux 内核大量使用。读者直接读,不加锁,不 CAS,不重试,零原子操作。写者复制一份改好后原子替换指针,等所有老读者退出后回收旧数据。
Go 中的近似实现:
var config atomic.Pointer[Config]
// 读者:一次 Load,零开销
cfg := config.Load()
// 写者:复制 → 修改 → 原子替换,不影响正在读的人
newCfg := *config.Load()
newCfg.Timeout = 30
config.Store(&newCfg)
读路径只有一次指针 Load,是最极致的读优化。适合配置热更新、路由表等读远多于写的场景。
选型
| 场景 | 方案 |
|---|---|
| 通用互斥 | sync.Mutex,简单可靠 |
| 简单计数器 | atomic.AddInt64(wait-free) |
| 条件更新(check-then-act) | CAS 循环 |
| 高竞争计数 | 分片计数器 |
| 读多写极少的共享状态 | atomic.Pointer(RCU 思路) |
大部分场景 sync.Mutex 就够了。无锁技术只在 热点路径 + 性能瓶颈已确认 时才值得引入,否则是用复杂度换不存在的性能问题。