再过30分钟你就能了解 go语言中的 原子操作 atomic 啦

134 阅读7分钟

一、原子操作

原子操作是变量级别的互斥锁。

同一时刻,只能有一个 CPU 对变量进行读或写。 当我们想要对某个变量做并发安全的修改,除了使用官方提供的 Mutex,还可以使用 sync/atomic 包的原子操作,可以把 sync/atomic 包中的原子操作看成是变量级别的互斥锁,它能够保证对变量的读取或修改期间不被其他的协程所影响。

二、mutex与atomic

在 Go(甚至是大部分语言)中,一条普通的赋值语句其实并不是一个原子操作(语言规范同样没有定义 i++ 是原子操作, 任何变量的赋值都不是原子操作)。例如,在 32 位机器上写 int64类型的变量是有中间状态的,它会被拆成两次写操作 MOV —— 写低 32 位和写高 32 位

图片.png

一种解决方法就是上锁,另一种就是利用 atomic 特性。

atomic 会比 mutex 有什么好处呢:mutex操作系统实现,而 atomic 包中的原子操作则由底层硬件直接提供支持。在 CPU 实现的指令集里,有一些指令被封装进了 atomic 包,这些指令在执行的过程中是不允许中断(interrupt)的,因此原子操作可以在 lock-free 的情况下保证并发安全,并且它的性能也能做到随 CPU 个数的增多而线性扩展。

对于现代的多处理多核的系统来说,一个核对地址的值的更改,在更新到主内存中之前,存放在在多级缓存中。这时候,其它的核看的的可能是还没有更新的数据。

多核处理器为了解决这种问题,使用了一种内存屏障的方式。使用这种方式后,数据的读写机制有点类似读写锁,并且写操作还会让 CPU 缓存失效,以便其它核能从主内存中拉取最新的值。

atomic 包提供的方法会提供内存屏障的功能,所以,atomic 不仅仅可以保证赋值的数据完整性,还能保证数据的可见性,一旦一个核更新了该地址的值,其它处理器总是能读取到它的最新值。

若实现相同的功能,atomic通常会更有效率,并且更能利用计算机多核的优势。所以,以后当我们想并发安全的更新一些变量的时候,我们应该优先选择用 atomic 来实现。

二、原子操作的使用场景是什么

原子操作本质上是一种变量级别的互斥锁。 因此,原子操作的使用场景也是和互斥锁类似的,但是不一样的是,我们的锁粒度只是一个变量而已。 也就是说,当我们不允许多个 CPU 同时对变量进行读写的时候(保证变量同一时刻只能一个 CPU 操作),就可以使用原子操作。

单核情况下,即使一个操作翻译成汇编不止一个指令,也有可能保持一致性。比如经常用来演示的并发场景下的 count++ 操作 (count++ 对应的汇编指令就有三条),如果像下面这样,无论执行多少次,输出结果都是 2000。

func main() {
    runtime.GOMAXPROCS(1)

    var w sync.WaitGroup
    count := int32(0)
    w.Add(100)
    for i := 0; i < 100; i++ {
        go func() {
            for j := 0; j < 20; j++ {
                count++
            }
            w.Done()
        }()
    }
    w.Wait()
    fmt.Println(count)
}

而在多核情况中,A核修改 count 的时候,由于 CPU 缓存的存在,B核读到的 count 值可能不是最新的值。如果我们将上面的例子中的第二行改成:

runtime.GOMAXPROCS(2)

之后,程序每执行一次,结果都有可能不一样。

解决思路除了使用 Mutex 加锁,也可以使用 atomic,具体使用方法是将 count++ 替换成:

atomic.AddInt32(&count, 1)

这样就能保证即使在多核情况下 count++ 也是一个原子操作。

三、 atomic 基本方法

3.1、增减 Add

  1. 用于进行增加或减少的原子操作,函数名以 Add 为前缀,后缀针对特定类型的名称。
  2. 原子增被操作的类型只能是数值类型,即 int32int64uint32uint64uintptr
  3. 原子增减函数的第一个参数为原值,第二个参数是要增减多少。
  4. int32int64 的第二个参数可以是负数,这样就可以做原子减法了。
func AddInt32(addr *int32, delta int32) (new int32)
func AddInt64(addr *int64, delta int64) (new int64)
func AddUint32(addr *uint32, delta uint32) (new uint32)
func AddUint64(addr *uint64, delta uint64) (new uint64)
func AddUintptr(addr *uintptr, delta uintptr) (new uintptr)

3.2、交换 Swap

SwapStore 有点类似,但是它会返回 *addr 的旧值

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

3.3、 比较并交换 CAS(CompareAndSwap)

CAS 操作中,会需要拿 new 的值跟 old 比较,如果相等,就将 new 赋值给 addr。 如果不相等,则不做任何操作。最后返回一个 bool 值,表示是否成功 swap

CompareAndSwap 的功能:

  1. 用于比较并交换的原子操作,函数名以 CompareAndSwap 为前缀,后缀针对特定类型的名称。
  2. 原子比较并交换被操作的类型可以是数值类型或指针类型,即 int32int64uint32uint64uintptrunsafe.Pointer
  3. 原子比较并交换函数的第一个参数为原值指针,第二个参数是要比较的值,第三个参数是要交换的值。
func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
func CompareAndSwapInt64(addr *int64, old, new int64) (swapped bool)
func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (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)

CAS最大的一个缺点就是:CAS 有自旋锁,如果不成功会一直循环,可能会给 cpu 带来很大开销。

3.4、 载入 Load

Load 方法会取出 addr 地址中的值,意味着读取值的同时,当前计算机的任何 CPU 都不会进行针对值的读写操作,即使在多处理器、多核、有 CPU cache 的情况下,Load 方法能保证数据的读一致性。

如果不使用原子 Load,当使用 v := value 这种赋值方式为变量 v 赋值时,读取到的 value 可能不是最新的,因为在读取操作时其他协程对它的读写操作可能会同时发生。

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

3.5、存储 Store

Store 可以将 val 值保存到 *addr 中,Store 操作是原子性的,因此在执行 Store 操作时,当前计算机的任何 CPU 都不会进行针对 *addr 的读写操作,其它协程通过 Load 读取数据,不会看到存取了一半的值

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

3.6、 原子操作任意类型的值 Value类型

上面的几种方法只支持基本的几种类型,因此,atomic 还提供了一个 Value 类型,它可以实现对任意类型(结构体)原子的存取操作,相比于上面的 StoreXXX 和 LoadXXX,value的操作效率会低一些,不过胜在简单易用。

// A Value provides an atomic load and store of a consistently typed value.
// The zero value for a Value returns nil from Load.
// Once Store has been called, a Value must not be copied.
//
// A Value must not be copied after first use.
type Value struct {
    v interface{}
}

func (v *Value) Load() (x interface{})
func (v *Value) Store(x interface{})