[每周一更]-(第152期):Go中的CAS(Compare-And-Swap)锁原理详解
CAS锁基本概念
CAS锁(Compare-And-Swap Lock)是一种基于原子比较交换操作实现的轻量级同步机制,属于自旋锁的一种实现方式。
核心特点
- 非阻塞同步:线程不会主动挂起,而是通过循环尝试获取锁
- 硬件支持:直接利用CPU的原子指令实现
- 乐观锁策略:假设竞争不激烈,适合短临界区场景
CAS基本概念
CAS(Compare-And-Swap,比较并交换)是一种原子操作,它是现代并发编程的基石之一。CAS操作包含三个基本操作数:
- 内存位置(V)
- 预期原值(A)
- 新值(B)
操作语义:“我认为位置V的值应该是A,如果是,那么将V的值更新为B,否则不做任何修改并告诉我当前实际值”
CAS在Go中的实现
在Go语言中,CAS操作主要通过sync/atomic
包实现:
package atomicfunc CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
func CompareAndSwapInt64(addr *int64, old, new int64) (swapped bool)
func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool)
// 其他类型的CAS操作...
CAS的工作原理
CAS操作的伪代码表示:
func CompareAndSwap(addr *T, old, new T) bool {if *addr == old {*addr = newreturn true}return false
}
关键点:
- 整个操作是原子性的(由CPU特殊指令保证)
- 操作是乐观的:假设不会发生冲突,冲突时重试
- 无阻塞:线程不会主动挂起
用CAS实现自旋锁
type SpinLock struct {state *int32 // 0表示未锁定,1表示锁定
}const free = 0
const locked = 1func (l *SpinLock) Lock() {for !atomic.CompareAndSwapInt32(l.state, free, locked) {// 可加入runtime.Gosched()避免过度占用CPU}
}func (l *SpinLock) Unlock() {atomic.StoreInt32(l.state, free)
}
CAS与互斥锁(Mutex)的区别
特性 | CAS自旋锁 | 互斥锁 |
---|---|---|
实现方式 | 循环尝试直到成功(硬件原子操作) | 操作系统系统调用 |
阻塞方式 | 忙等待(消耗CPU) | 线程挂起(不消耗CPU) |
适用场景 | 临界区操作非常快 | 临界区操作耗时 |
线程切换 | 无 | 有 |
内存开销 | 很小 | 较大 |
公平性 | 通常不公平 | 可实现公平 |
CAS锁关键特性
特性 | 说明 |
---|---|
原子性 | 由CPU指令保证比较和交换操作的原子性 |
自旋等待 | 获取不到锁时会循环重试而非线程挂起 |
无调度开销 | 不涉及操作系统内核调度 |
内存一致性 | 保证对锁变量的修改对所有线程立即可见 |
轻量级 | 相比互斥锁(Mutex)消耗更少资源 |
CAS的典型应用场景
适合场景
- 临界区操作非常快(纳秒/微秒级)
- 低竞争环境(线程争用不激烈)
- 需要极高性能的场景
典型用例
- 内核数据结构保护
- 无锁数据结构实现基础
- 高性能计数器
- 状态标志更新
具体示例
-
计数器原子递增
func atomicIncrement(addr *int32) {for {old := atomic.LoadInt32(addr)new := old + 1if atomic.CompareAndSwapInt32(addr, old, new) {return}} }
-
无锁数据结构:如无锁队列、无锁栈等
-
状态标志更新:当需要基于当前状态原子性更新到新状态时
-
Ticket Lock(票号锁)
解决公平性问题,按请求顺序分配锁
type TicketLock struct {next int32owner int32 }func (l *TicketLock) Lock() {t := atomic.AddInt32(&l.next, 1) - 1for atomic.LoadInt32(&l.owner) != t {runtime.Gosched()} }
CAS的优缺点
优点:
- 无上下文切换:无阻塞,线程不会挂起
- 低延迟:轻量级,适合简单操作,获取锁的速度快(约10-100个CPU周期)
- 可扩展性:在多核系统上表现良好,避免死锁问题(因为没有持有锁的概念)
缺点:
- ABA问题:值从A变为B又变回A,CAS会误认为没变化
- 解决方法:使用版本号或标记指针
- 循环开销:高竞争时大量CPU浪费在重试
- 只能保护一个变量:复杂操作需要多个CAS组合
Go中的CAS实战示例
实现线程安全的计数器
type Counter struct {value int64
}func (c *Counter) Increment() {for {old := atomic.LoadInt64(&c.value)new := old + 1if atomic.CompareAndSwapInt64(&c.value, old, new) {return}}
}func (c *Counter) Value() int64 {return atomic.LoadInt64(&c.value)
}
实现简单的自旋锁
type SpinLock struct {flag int32
}func (sl *SpinLock) Lock() {for !atomic.CompareAndSwapInt32(&sl.flag, 0, 1) {// 可适当加入退让策略runtime.Gosched()}
}func (sl *SpinLock) Unlock() {atomic.StoreInt32(&sl.flag, 0)
}
CAS的底层硬件支持
现代CPU通常提供CAS指令:
- x86:
CMPXCHG
指令 - ARM:
LDREX/STREX
指令对 - Go的atomic包会针对不同平台使用最优实现
使用建议
- 简单操作用CAS:如标志设置、计数器增减
- 复杂操作用Mutex:当需要保护多个变量或复杂逻辑时
- 注意竞争程度:高竞争场景CAS性能可能反而不如Mutex
- 避免过度自旋:长时间获取不到锁应退让或切换策略
为什么需要CAS
- 无锁编程基础:构建无锁数据结构(队列、栈等)的必要原语
- 性能优势:比互斥锁更轻量级的同步方式
- 硬件映射:直接利用CPU的原子指令能力
- 并发控制:实现更高效的同步机制(如自旋锁、信号量等)
CAS是构建无锁算法的基础,理解CAS对深入掌握并发编程至关重要。在Go中,大多数时候
sync.Mutex
已经足够好,但在性能关键路径上,合理使用CAS可以带来显著性能提升。Go的sync.Mutex
:早期版本使用纯自旋,现在实现为自适应锁