Golang 原子操作

144 阅读6分钟

概念

一个或者多个操作在CPU执行的过程中不被中断的特性,称为原子性(atomicity)。这些操作对外表现成一个不可分割的整体,它们要么都执行,要不都不执行,中间状态对外不可见。

操作类型

  • 增减,保证对操作数进行原子的增减,支持的类型为:int32、int64、uint32、uint64、uintptr。方法名:AddXXXType
  • 载入,保证读取到操作数前没有其他任务对它进行更改,支持类型:基础类型、任意类型指针。方法名:LoadXXXType
  • 存储,支持类型:基础类型、任意类型指针。方法名:StoreXXXType
  • 比较并交换(CompareAndSwap),支持类型:基础类型、任意类型指针。
  • 交换,没有比较,直接交换。

与互斥锁的区别

  • 使用目的:互斥锁是用于保护一段逻辑,原子操作是用于对一个变量的更新保护。
  • 底层实现:Mutex由操作系统的调度器实现,atomic包中的原子操作由底层硬件指令直接提供支持,这些指令在执行过程中是不允许中断的。因此原子操作可以在Lock-Free(无锁编程)的情况下保证并发安全,并且性能也能做到随CPU个数的增多而线性扩展。

实例

func AtomicAdd() {
   var a int32 =  0
   var wg sync.WaitGroup
   var mu sync.Mutex
   start := time.Now()
   for i := 0; i < 100000000; i++ {
      wg.Add(1)
      go func() {
         defer wg.Done()
         // mutex
         mu.Lock()
         a += 1
         mu.Unlock()
         // atomic
         atomic.AddInt32(&a, 1)
      }()
   }
   wg.Wait()
   timeSpends := time.Now().Sub(start).Nanoseconds()
   fmt.Printf("use mutex a is %d, spend time: %v\n", a, timeSpends)
   // atomic load
   fmt.Printf("use mutex a is %d, spend time: %v\n", atomic.LoadInt32(&a), timeSpends)
}

以上代码皆是线程安全的。所有的原子操作方法的被操作数形参都必须是指针类型,通过指针变量可以获取被操作数在内存中的地址,从而施加特殊的CPU指令,确保同一时间只有一个goroutine能够执行操作。

结构体操作

需要并发安全的设置一个结构体的多个字段:

  • 把结构体转换成指针,通过 StorePointer 设置
  • 使用 atomic.Value,在底层完成了从具体指针类型到 unsafe.Pointer之间的转换。

atomic.Value 对外方法

  • v.Store(c) - 写操作,将原始的变量c存放到一个 atomic.value 类型的v里。
  • c := v.Load() - 读操作,从线程安全的v中读取上一步存放的内容。

atomic.Value的内部实现

atomic.Value 内部的字段是一个interface,用于存储任意类型的数据。

type Value struct {
   v interface{}
}

atomic包内部定义了一个ifaceWords类型,这其实是interface{}的内部表示 (runtime.eface),它的作用是将interface{}类型分解,得到其原始类型(typ)和真正的值(data)。

// ifaceWords is interface{} internal representation.
type ifaceWords struct {
   typ  unsafe.Pointer
   data unsafe.Pointer
}

写入线程安全的保证

unsafe.Pointer:Go不支持直接操作内存,但是提供了一种不保证向后兼容性的指针类型unsafe.Pointer,用于程序灵活的操作内存。

unsafe.Pointer可绕过Go类型系统的检查,与任意的指针类型互相转换。只要两种类型具有相同的内存结构(layout),就可以使用unsafe.Pointer作为中间媒介,让两种类型的指针相互转换,从而实现同一份内存拥有两种不同的解读方式。

[]byte与string 内部存储结构一样,运行时分别表示为 reflect.SliceHeader 和 reflect.StringHeader

type SliceHeader struct {
   Data uintptr
   Len  int
   Cap  int
}

type StringHeader struct {
   Data uintptr
   Len  int
}

零拷贝直接转换

bytes := []byte{104, 101, 108, 108, 111}

p := unsafe.Pointer(&bytes) //将 *[]byte 指针强制转换成unsafe.Pointer
str := *(*string)(p) //将 unsafe.Pointer再转换成string类型的指针,再将这个指针的值当做string类型取出来
fmt.Println(str) //输出 "hello"

底层实现

func (v *Value) Store(x interface{}) {
   if x == nil {
      panic("sync/atomic: store of nil value into Value")
   }
   vp := (*ifaceWords)(unsafe.Pointer(v))  // Old value
   xp := (*ifaceWords)(unsafe.Pointer(&x)) // New value
   for {
      typ := LoadPointer(&vp.typ)
      if typ == nil {
         // Attempt to start first store.
         // Disable preemption so that other goroutines can use
         // active spin wait to wait for completion; and so that
         // GC does not see the fake type accidentally.
         runtime_procPin()
         if !CompareAndSwapPointer(&vp.typ, nil, unsafe.Pointer(^uintptr(0))) {
            runtime_procUnpin()
            continue
         }
         // Complete first store.
         StorePointer(&vp.data, xp.data)
         StorePointer(&vp.typ, xp.typ)
         runtime_procUnpin()
         return
      }
      if uintptr(typ) == ^uintptr(0) {
         // First store in progress. Wait.
         // Since we disable preemption around the first store,
         // we can wait with active spinning.
         continue
      }
      // First store completed. Check type and overwrite data.
      if typ != xp.typ {
         panic("sync/atomic: store of inconsistently typed value into Value")
      }
      StorePointer(&vp.data, xp.data)
      return
   }
}

大致逻辑:

  • 通过 unsafe.Pointer 将现有的和要写入的值分别转成 ifaceWords 类型,后续可以得到这两个 interface{} 的原始类型(typ)和真正的值(data)
  • for无限循环,配合CompareAndSwap 使用,可以达到乐观锁的效果。
  • 通过 LoadPointer 原子操作拿到当前 Value 中存储的类型,后续根据类型不同分三种情况处理:
  1. 第一次写入 - 一个atomic.Value 实例被初始化后,它的typ字段会被设置为指针的零值nil,所以先判断如果typ是nil那就证明这个Value实例还未被写入过数据。然后初始写入的操作:
  • runtime_proPin() 是runtime中的一段函数,一方面它禁止了调度器对当前goroutine 的抢占(preemption),使得它在执行当前逻辑的时候不被打断,以便尽快完成工作,其余的任务都在等待它完成。另一方面,在禁止抢占期间,GC线程也无法被启用,可以防止GC线程指向 ^uintptr(0) 的类型(赋值过程的中间状态)。
  • 使用CAS操作,尝试将typ设置为 ^uintptr(0) 中间状态。如果失败,则证明已经有别的线程抢先完成了赋值操作,那就解除抢占锁,重新回到for循环第一步。
  • 如果设置成功,那就证明当前线程抢到这个“乐观锁”,它可以安全的把v设为传入的新值。先写data字段,然后再写typ字段。是因为以typ字段的值作为写入完成与否的判断标准。
  1. 第一次写入还未完成 - 如果tyo字段还是 ^uintptr(0) 这个中间态,证明第一次写入还未完成,所以会继续循环,一直等到第一次写入完成。
  2. 第一次写入已经完成 - 首先检查上一次写入的类型与这次要写入的类型是否一致,如果不一致则抛出异常。繁殖,则直接把这一次的值写入data字段。

核心思想:为了完成多个字段的原子性写入,以其中一个字段的状态来标志整个原子写入的状态。

func (v *Value) Load() (x interface{}) {
   vp := (*ifaceWords)(unsafe.Pointer(v))
   typ := LoadPointer(&vp.typ)
   if typ == nil || uintptr(typ) == ^uintptr(0) {
      // First store not yet completed.
      return nil
   }
   data := LoadPointer(&vp.data)
   xp := (*ifaceWords)(unsafe.Pointer(&x))
   xp.typ = typ
   xp.data = data
   return
}

大致逻辑:

  1. 如果当前的typ是nil或者 ^uintptr(0) ,证明第一次写入还没有开始或者还没完成,直接返回nil(不对外暴露中间状态)。
  2. 否则,根据当前得到的typ和data构造出一个新的 interface{} 返回。