go语法:sync/atomaic原子操作

参考:

https://www.jianshu.com/p/6ca885ede2a8(sync/atomaic原子操作)

https://zhuanlan.zhihu.com/p/401606797(知乎:atomic原子操作)

核心概念:

原子性:一个或多个操作在CPU的执行过程中不被中断的特性,称为原子性。这些操作对外表现成一个不可分割的整体,他们要么都执行,要么都不执行,外界不会看到他们只执行到一半的状态。

原子操作:进行过程中不能被中断的操作,原子操作由底层硬件支持,而锁则是由操作系统提供的API实现,若实现相同的功能,前者通常会更有效率

协程并发问题

在goroutine中访问外部的变量并不安全,我们先看看下面这个例子,我们执行一次计数,使用sync.WaitGroup包保证我们创建的1000个goroutine全部执行完毕后再输出n,运行程序看看结果如何:

func no_atomic()  {
    var n int32
    var wg sync.WaitGroup
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            n++
            wg.Done()
        }()
    }
    wg.Wait()

    fmt.Println(atomic.LoadInt32(&n)) // 978
}

电脑多核的情况下,不限制cpu执行上面代码,每次执行的结果不是1000,而是小于等于一千数值波动。为什么会出现这种情况呢?上面我们通过for循环里创建1000个goroutine,每个goroutine都将n加上1,加入有其中两个goroutine如下执行步骤:

  • goroutine1读取变量n 值为800
  • goroutine2读取变量n 值为800
  • goroutine1执行n+1,n变为801
  • goroutine2执行n+1,n变为801
  • 两次goroutine执行完毕
  • 结果n少加了一次1,所以最终的结果就比预期少1

sync/atomic的原子操作,主要有五大类:

Load:返回原值

1,该类方法主要负责从相应的内存地址中获取对应的值

2,Load 方法是为了防止在读取过程中,有其他协程发起修改动作,影响了读取结果,常用于配置项的整个读取

3,读取的时候,其他协程不能写入

func LoadInt32(addr *int32) (val int32)
func LoadInt64(addr *int64) (val int64)
func LoadUint32(addr *uint32) (val uint32)
func LoadUint64(addr *uint64) (val uint64)
func LoadUintptr(addr *uintptr) (val uintptr)
func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer)

Store:无返回值

1,整体赋值

2,有原子读取,就有原子修改值,前面提到过的 Add 只适用于 int、uint 类型的增减,并没有其他类型的修改,而 Sotre 方法通过 unsafe.Pointer 指针原子修改,来达到了对其他类型的修改。

3,写入的时候,其他协程不能读取

func StoreInt32(addr *int32, val int32)
func StoreInt64(addr *int64, val int64)
func StoreUint32(addr *uint32, val uint32)
func StoreUint64(addr *uint64, val uint64)
func StoreUintptr(addr *uintptr, val uintptr)
func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer)

Add:返回新值

1,原子增减

2,可以理解为 先load,再+=,再store,三者中间不会打断

func AddInt32(addr *int32, delta int32) (new int32)
func AddUint32(addr *uint32, delta uint32) (new uint32)
func AddInt64(addr *int64, delta int64) (new int64)
func AddUint64(addr *uint64, delta uint64) (new uint64)
func AddUintptr(addr *uintptr, delta uintptr) (new uintptr)

Swap:返回旧值

1,原子赋值

2,可以理解为 先load旧值,再Store新值,然后返回旧值

func SwapInt32(addr *int32, new int32) (old int32)
func SwapInt64(addr *int64, new int64) (old int64)
func SwapUint32(addr *uint32, new uint32) (old uint32)
func SwapUint64(addr *uint64, new uint64) (old uint64)
func SwapUintptr(addr *uintptr, new uintptr) (old uintptr)
func SwapPointer(addr *unsafe.Pointer, new unsafe.Pointer) (old unsafe.Pointer)

CompareAndSwap:返回bool值

1,该操作在进行交换前首先确保被操作数的值未被更改,即仍然保存着参数 old 所记录的值,满足此前提条件下才进行交换操作。

2,CAS的做法类似操作数据库时常见的乐观锁机制。

3,当有大量的goroutine 对变量进行读写操作时,可能导致CAS操作无法成功,这时可以利用for循环多次尝试。

4,CompareAndSwap 有可能产生 ABA 现象发生。也就是原来的值是 A,后面被修改 B,再后面修改为 A。在这种情况下也符合了 CompareAndSwap 规则,即使中途有被改动过。

func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
func CompareAndSwapInt64(addr *int64, old, new int64) (swapped bool)
func CompareAndSwapUint32(addr *uint32, old, new uint32) (swapped bool)
func CompareAndSwapUint64(addr *uint64, old, new uint64) (swapped bool)
func CompareAndSwapUintptr(addr *uintptr, old, new uintptr) (swapped bool)
func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool)

atomic包中支持六种类型

int32
uint32
int64
uint64
uintptr
unsafe.Pointer

对于每一种类型,提供了五类原子操作:

LoadXXX(addr): 原子性的获取*addr的值,等价于:

return *addr    

StoreXXX(addr, val): 原子性的将val的值保存到*addr,等价于:

addr = val 

AddXXX(addr, delta): 原子性的将delta的值添加到*addr并返回新值(unsafe.Pointer不支持),等价于:

*addr += delta
return *addr

SwapXXX(addr, new) old: 原子性的将new的值保存到*addr并返回旧值,等价于:

old = *addr
*addr = new
return old

CompareAndSwapXXX(addr, old, new) bool: 原子性的比较*addr和old,如果相同则将new赋值给*addr并返回true,等价于:

if *addr == old {
    *addr = new
    return true
}
return false

使用sync/atomic解决文首的问题,代码如下:

func _atomic()  {
    var n int32
    var wg sync.WaitGroup
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            atomic.AddInt32(&n, 1)
            wg.Done()
        }()
    }
    wg.Wait()

    fmt.Println(atomic.LoadInt32(&n)) // 1000
}

使用 atomic.AddInt32进行加操作,最终输出预期的结果, 其实atomic包中的方法在执行完毕之前不会被其他的任务或者事件中断,该操作是并发安全,在向此地址写入或者读取值时原子性的操作,

如果其他goroutine尝试读取或写入会被阻塞,直到此次原子操作完成。

各类方法的应用场景,在实际应用中发掘和体会