Go开发atomic之比较并交换操作(CAS)
有关CAS的文章,网络有很多详细说明,这里只做一个简洁的整理原理
比较并交换称为CAS ,如图所示:
如图所示,先从变量v中读取值,然后当修改时,就拿取的值再和内存中的值比一下。
这个也容易理解,比如说,我想修改的值是以原来取的那个值为参照的,如果当前这两个值不一样了,肯定是被别人改了 。因此,我不得不重新读取一次,再来修改,以此循环。
在这个故事中,还有一种情况,如果v被别人改了之后又再次改回来了还是v。那我方还以为v从来没变过,这就是ABA问题。修改上一篇的代码
上篇讲了一个例子,两个协程分别将整数n循环加5000次,我们用比较并交换来修改下:var n int32 = 0 sig := make(chan int) go func() { //看下尝试多少次 nTry := 0 for i := 0; i < 5000; i++ { for { old := n if atomic.CompareAndSwapInt32(&n, old, old+1) { break } else { nTry++ } } } fmt.Printf("nTry=%v ", nTry) sig <- 0 }() go func() { //看下尝试多少次 nTry := 0 for i := 0; i < 5000; i++ { for { old := n if atomic.CompareAndSwapInt32(&n, old, old+1) { break } else { nTry++ } } } fmt.Printf("nTry=%v ", nTry) sig <- 0 }() <-sig <-sig fmt.Println(n)
加一个for循环的原因是,可能一次没有成功,还需要重新尝试。
用这种模式也可以解决同步的问题Go中的CAS源码
实际代码文件在/src/runtime/internal/atomic/asm_amd64.s文件中TEXT runtime∕internal∕atomic·Cas64(SB), NOSPLIT, $0-25 MOVQ ptr+0(FP), BX MOVQ old+8(FP), AX MOVQ new+16(FP), CX LOCK // 比较BX和AX中的值,如果相等,将CX中的值给BX,即*addr=new CMPXCHGQ CX, 0(BX) // 设置返回值swapped,CMPXCHGQ比较如果相等,ret为1,否则为0 SETEQ ret+24(FP) RET
其中我们可以看作lock(一个命令前缀,在这里用于CMPXCHGQ) 可以锁住总线保证多次内存操作的原子性,然后执行CMPXCHGQ
CMPXCHGQ CX, 0(BX)的解释 :如果AX(旧)与BX(原)相等,则CX(新)送BX且ZF置1;否则BX送给CX,且ZF清0
因此,比较并交换是依赖硬件完成的CAS的优缺点
优点:乐观锁,轻量
缺点:解决不了ABACAS如果不成功则会发生自旋,但是自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。只能保证一个共享变量的原子操作