[每周一更]-(第152期):Go中的CAS(Compare-And-Swap)锁原理详解

发布于:2025-08-01 ⋅ 阅读:(13) ⋅ 点赞:(0)

在这里插入图片描述

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 atomic

func 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 = new
        return true
    }
    return false
}

关键点

  1. 整个操作是原子性的(由CPU特殊指令保证)
  2. 操作是乐观的:假设不会发生冲突,冲突时重试
  3. 无阻塞:线程不会主动挂起

用CAS实现自旋锁

type SpinLock struct {
    state *int32 // 0表示未锁定,1表示锁定
}

const free = 0
const locked = 1

func (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的典型应用场景

适合场景

  • 临界区操作非常快(纳秒/微秒级)
  • 低竞争环境(线程争用不激烈)
  • 需要极高性能的场景

典型用例

  1. 内核数据结构保护
  2. 无锁数据结构实现基础
  3. 高性能计数器
  4. 状态标志更新

具体示例

  1. 计数器原子递增

    func atomicIncrement(addr *int32) {
        for {
            old := atomic.LoadInt32(addr)
            new := old + 1
            if atomic.CompareAndSwapInt32(addr, old, new) {
                return
            }
        }
    }
    
  2. 无锁数据结构:如无锁队列、无锁栈等

  3. 状态标志更新:当需要基于当前状态原子性更新到新状态时

  4. Ticket Lock(票号锁)

    解决公平性问题,按请求顺序分配锁

    type TicketLock struct {
    	next  int32
    	owner int32
    }
    
    func (l *TicketLock) Lock() {
    	t := atomic.AddInt32(&l.next, 1) - 1
    	for 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 + 1
        if 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包会针对不同平台使用最优实现

使用建议

  1. 简单操作用CAS:如标志设置、计数器增减
  2. 复杂操作用Mutex:当需要保护多个变量或复杂逻辑时
  3. 注意竞争程度:高竞争场景CAS性能可能反而不如Mutex
  4. 避免过度自旋:长时间获取不到锁应退让或切换策略

为什么需要CAS

  1. 无锁编程基础:构建无锁数据结构(队列、栈等)的必要原语
  2. 性能优势:比互斥锁更轻量级的同步方式
  3. 硬件映射:直接利用CPU的原子指令能力
  4. 并发控制:实现更高效的同步机制(如自旋锁、信号量等)

CAS是构建无锁算法的基础,理解CAS对深入掌握并发编程至关重要。在Go中,大多数时候sync.Mutex已经足够好,但在性能关键路径上,合理使用CAS可以带来显著性能提升。Go的sync.Mutex:早期版本使用纯自旋,现在实现为自适应锁


网站公告

今日签到

点亮在社区的每一天
去签到