1. 什么是原子操作 程序的原子操作一般指:程序的一条或者几条指令是一个不可分割的执行整体,要么全部执行,要么不执行。
在Go中有原子操作对应的包sync/atomic,可以用来对某些具有竞争性的变量进行原子操作,以免在多goroutine操作的情况下数据发生错乱,而不能达到预期要求。
2. 多goroutine加法的两种操作 普通的加法操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 func main() { var num uint32 = 0 var add uint32 = 1 wg := sync.WaitGroup{} wg.Add(100) for i:=0; i < 100; i++{ go func() { defer wg.Done() num += add }() } wg.Wait() fmt.Println(num) }
执行结果基本都是 num != 100。
使用sync/atomic包进行原子加法操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 func main() { var num uint32 = 0 var add uint32 = 1 wg := sync.WaitGroup{} wg.Add(100) for i:=0; i < 100; i++{ go func() { defer wg.Done() atomic.AddUint32(&num, add) }() } wg.Wait() fmt.Println(num) }
执行结果都为 num == 100。
3. 从汇编层面分析 为什么会出现上面的执行结果呢?
首先分析一下num += add
的汇编代码:
1 2 3 MOVL 0(SP), AX ;获取num的值 INCL AX ; 进行加1操作 MOVL AX, 0(SP) ;把结果赋值给num
也就是说,num += add
对应3条底层CPU指令,由于多goroutine是并发执行并没有采取其他同步机制,导致同时会有多个goroutine取到相同的num值,并进行加1操作。这样就导致看到的结果不等于100。
在看一下原子操作atomic.AddUint32(&num, add)
对应的汇编:
1 2 3 MOVQ 0x18(SP), AX ;num的地址放到寄存器AX MOVL $0x1, CX ;累加的值1放到寄存器CX LOCK XADDL CX, 0(AX) ;进行相加操作(核心操作)
也是3条底层CPU指令,但是与上面的3条CPU指令却有本质的区别。
LOCK XADDL CX, 0(AX)
中LOCK前缀标识该指令为原子操作指令(由硬件进行支持),XADDL表示交换并相加(相当于num += add
对应的3条底层CPU指令)是一个不可分割的原子操作。由于LOCK的加持,就算是多goroutine同时执行到了该CPU指令的位置,也只有一个goroutine能执行成功,其他goroutine都要进行等待。这也就是使用atomic执行加法操作能够达到预期的原因。
4. go源码中AddUint32的实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 // 源码位置:sync/atomic/doc.go // AddUint32 atomically adds delta to *addr and returns the new value. // To subtract a signed positive constant value c from x, do AddUint32(&x, ^uint32(c-1)). // In particular, to decrement x, do AddUint32(&x, ^uint32(0)). func AddUint32(addr *uint32, delta uint32) (new uint32) // 源码位置:sync/atomic/asm.s TEXT ·AddUint32(SB),NOSPLIT,$0 JMP runtime∕internal∕atomic·Xadd(SB) // 最终源码位置(amd64架构):runtime/internal/atomic/asm_amd64.s // uint32 Xadd(uint32 volatile *val, int32 delta) // Atomically: // *val += delta; // return *val; TEXT runtime∕internal∕atomic·Xadd(SB), NOSPLIT, $0-20 MOVQ ptr+0(FP), BX MOVL delta+8(FP), AX MOVL AX, CX LOCK XADDL AX, 0(BX) ADDL CX, AX MOVL AX, ret+16(FP) RET
可以看到原子操作的方法是由汇编代码实现(不同的硬件平台实现原子操作所提供的指令不一样),核心的指令还是 LOCK
与XADDL
。
sync/atomic包的其他方法,也是使用类似的汇编代码实现。
总结 通过上面代码以及汇编分析,可得以下结论:
原子操作是不可分割的整体(包含一条或者多条处理动作)
不同架构的原子操作,其实现指令不同
原子操作需要底层硬件支持(加锁一般由操作系统提供的API支持)