什么是 CAS

Page content

什么是 CAS

CAS 的全称是 Compare And Swap.

该指令会将给定的值和一个内存地址中的值进行比较,如果它们是同一个值,就使用新值替换内存地址中的值,整个操作过程是原子性的。

那啥是原子性呢?

原子性我们只需要记住一点,就是这个操作总是基于最新的值进行计算,如果同时有其它线程已经修改了这个值,那么这次操作不成功。

听起来还是很绕?贴个 golang 源码片段(还带汇编的哦)

// func cas(val *int32, old, new int32) bool
// Atomically:
//	if *val == old {
//		*val = new;
//		return true;
//	}else
//		return false;
TEXT sync·cas(SB), 7, $0
	MOVQ	8(SP), BX
	MOVL	16(SP), AX
	MOVL	20(SP), CX
	LOCK
	CMPXCHGL	CX, 0(BX)
	JZ ok
	MOVL	$0, 24(SP)
	RET
ok:
	MOVL	$1, 24(SP)
	RET

再贴一段早期的 Mutex 源码片段 源码地址

func xadd(val *uint32, delta int32) (new uint32) {
	for {
		v := *val;
		nv := v+uint32(delta);
		if cas(val, v, nv) {
			return nv;
		}
	}
	panic("unreached");
}

举个栗子巩固一下

package main

import (
	"fmt"
	"sync"
	//"sync/atomic"
)

var counter int32

func main() {
	wg := sync.WaitGroup{}
	wg.Add(1000)
	for i:=0; i<1000; i++ {
		go func() {
			defer wg.Done()
			counter++
			//atomic.AddInt32(&counter, 1)
		}()
	}
	wg.Wait()
	fmt.Println(counter)
}

运行上面这段代码,结果是随机的,问题出在 counter++ 语句不是原子操作,它实际的执行过程分为如下几个步骤:

  • step1: 查询 counter 当前值
  • step2: 计算 counter + 1
  • step3: 将计算结果赋值给 counter

下一步的操作依赖上一步操作中读取到的 counter 值,高并发场景下,当执行到 step2 时,counter 的值有可能已经被其它 goroutine 修改掉了,此时再执行 step3,会导致其它 goroutine 的修改被覆盖。

如果把 counter++ 操作修改成 xadd(&counter, 1),那执行结果将是确定的 1000。遗憾的是我们无法直接调用 xadd 方法,不过我们可以调用 golang 封装的一个原子操作 atomic.AddInt32(&counter, 1)