Go中sync/atomic原子操作分析

ivansli 2021/11/17 339℃ 0

1. 什么是原子操作

程序的原子操作一般指:程序的一条或者几条指令是一个不可分割的执行整体,要么全部执行,要么不执行。

在Go中有原子操作对应的包sync/atomic,可以用来对某些具有竞争性的变量进行原子操作,以免在多goroutine操作的情况下数据发生错乱,而不能达到预期要求。

2. 多goroutine加法的两种操作

普通的加法操作:

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包进行原子加法操作:

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的汇编代码:

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)对应的汇编:

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的实现

// 源码位置: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

可以看到原子操作的方法是由汇编代码实现(不同的硬件平台实现原子操作所提供的指令不一样),核心的指令还是 LOCKXADDL

sync/atomic包的其他方法,也是使用类似的汇编代码实现。

总结

通过上面代码以及汇编分析,可得以下结论:

  1. 原子操作是不可分割的整体(包含一条或者多条处理动作)
  2. 不同架构的原子操作,其实现指令不同
  3. 原子操作需要底层硬件支持(加锁一般由操作系统提供的API支持)
Golang

评论啦~