go语言标准库sync/atomic中的原子操作

1,240 阅读3分钟

引言

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

package main

import (
    "fmt"
    "sync"
    "sync/atomic"
)

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

    fmt.Println(n) // 每次执行的结果不是1000,小于等于一千数值波动

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

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

使用sync/atomic修改上述代码

我们使用sync/atomic原子操作解决上述并发安全问题,代码如下:


func main() {
    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尝试读取或写入会被阻塞,知道此次原子操作完成。

在sync/atomic中提供了许多原子操作支持,主要有五大类:

  1. Load:该类方法主要负责从相应的内存地址中获取对应的值
  2. Store:该类主要负责将对应的值保存在相应的内存地址中
  3. Add:该类方法可以理解是Load和Store的结合,也就是先Load然后Add
  4. Swap:该类方法可以理解为先Load,在Store新值,然后返回旧值
  5. CompareAndSwap:该类方法可以这样理解:先比较旧数据和地址中保存数据的值,如果相同的话,执行Swap,把新的数值保存在地址中,返回true,如果不同,那么直接返回false

常用方法

sync/atomic包中的方法都如下格式:

func AddT(addr *T, delta T)(new T)
func LoadT(addr *T) (val T)
func StoreT(addr *T, val T)
func SwapT(addr *T, new T) (old T)
func CompareAndSwapT(addr *T, old, new T) (swapped bool)

例如:

//	下面是in32的
func AddInt32(addr *int32, delta int32)(new int32)
func LoadInt32(addr *int32) (val int32)
func StoreInt32(addr *int32, val int32)
func SwapInt32(addr *int32, new int32) (old int32)
func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)

//	为(安全)指针类型提供以下四个原子函数:
//	并没有AddPointer函数,因为go的指针不支持算术运算
func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer)
func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer)
func SwapPointer(addr *unsafe.Pointer, new T) (old unsafe.Pointer)
func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool)