Go 的 Atomic.Value 为什么不加锁也能保证数据线程安全?

开发 后端
本文由浅入深的介绍了atomic.Value的使用姿势,以及内部实现。让大家不仅知其然,还能知其所以然。

[[434668]]

有些朋友可能没有注意过,在 Go(甚至是大部分语言)中,一条普通的赋值语句其实不是一个原子操作。例如,在32位机器上写int64类型的变量就会有中间状态,因为它会被拆成两次写操作(汇编的MOV指令)——写低 32 位和写高 32 位,如下图所示:

32机器上对int64进行赋值

如果一个线程刚写完低32位,还没来得及写高32位时,另一个线程读取了这个变量,那它得到的就是一个毫无逻辑的中间变量,这很有可能使我们的程序出现Bug。

这还只是一个基础类型,如果我们对一个结构体进行赋值,那它出现并发问题的概率就更高了。很可能写线程刚写完一小半的字段,读线程就来读取这个变量,那么就只能读到仅修改了一部分的值。这显然破坏了变量的完整性,读出来的值也是完全错误的。

面对这种多线程下变量的读写问题,Go给出的解决方案是atomic.Value登场了,它使得我们可以不依赖于不保证兼容性的unsafe.Pointer类型,同时又能将任意数据类型的读写操作封装成原子性操作。

之前我在文章Golang 五种原子性操作的用法详解里,详细介绍过它的用法,下面我们先来快速回顾一下atomic.Value的使用方式

atomic.Value的使用方式

atomic.Value类型对外提供了两个读写方法:

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

下面是一个简单的例子演示atomic.Value的用法。

  1. type Rectangle struct { 
  2.  length int 
  3.  width  int 
  4.  
  5. var rect atomic.Value 
  6.  
  7. func update(width, length int) { 
  8.  rectLocal := new(Rectangle) 
  9.  rectLocal.width = width 
  10.  rectLocal.length = length 
  11.  rect.Store(rectLocal) 
  12.  
  13. func main() { 
  14.  wg := sync.WaitGroup{} 
  15.  wg.Add(10) 
  16.  // 10 个协程并发更新 
  17.  for i := 0; i < 10; i++ { 
  18.   go func() { 
  19.    defer wg.Done() 
  20.    update(i, i+5) 
  21.   }() 
  22.  } 
  23.  wg.Wait() 
  24.  _r := rect.Load().(*Rectangle) 
  25.  fmt.Printf("rect.width=%d\nrect.length=%d\n", _r.width, _r.length) 

你也可以试试,不用atomic.Value,直接给Rectange类型的指针变量赋值,对比一下两者结果的区别。

你可能会好奇,为什么atomic.Value在不加锁的情况下就提供了读写变量的线程安全保证,接下来我们就一起看看其内部实现。

atomic.Value的内部实现

atomic.Value被设计用来存储任意类型的数据,所以它内部的字段是一个interface{}类型。

  1. type Value struct { 
  2.  
  3. v interface{} 
  4.  

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

  1. // ifaceWords is interface{} internal representation. 
  2. type ifaceWords struct { 
  3.   typ  unsafe.Pointer 
  4.   data unsafe.Pointer 

写入线程安全的保证

在介绍写入之前,我们先来看一下 Go 语言内部的unsafe.Pointer类型。

unsafe.Pointer

出于安全考虑,Go 语言并不支持直接操作内存,但它的标准库中又提供一种不安全(不保证向后兼容性) 的指针类型unsafe.Pointer,让程序可以灵活的操作内存。

unsafe.Pointer的特别之处在于,它可以绕过 Go 语言类型系统的检查,与任意的指针类型互相转换。也就是说,如果两种类型具有相同的内存结构(layout),我们可以将unsafe.Pointer当做桥梁,让这两种类型的指针相互转换,从而实现同一份内存拥有两种不同的解读方式。

比如说,[]byte和string其实内部的存储结构都是一样的,他们在运行时类型分别表示为reflect.SliceHeader和reflect.StringHeader

  1. type SliceHeader struct { 
  2.  Data uintptr 
  3.  Len  int 
  4.  Cap  int 
  5.  
  6. type StringHeader struct { 
  7.  Data uintptr 
  8.  Len  int 

但 Go 语言的类型系统禁止他俩互换。如果借助unsafe.Pointer,我们就可以实现在零拷贝的情况下,将[]byte数组直接转换成string类型。

  1. bytes := []byte{104, 101, 108, 108, 111} 
  2.  
  3. p := unsafe.Pointer(&bytes) //将 *[]byte 指针强制转换成unsafe.Pointer 
  4. str := *(*string)(p) //将 unsafe.Pointer再转换成string类型的指针,再将这个指针的值当做string类型取出来 
  5. fmt.Println(str) //输出 "hello" 

知道了unsafe.Pointer的作用,我们可以直接来看代码了:

  1. func (v *Value) Store(x interface{}) { 
  2.   if x == nil { 
  3.     panic("sync/atomic: store of nil value into Value"
  4.   } 
  5.   vp := (*ifaceWords)(unsafe.Pointer(v))  // Old value 
  6.   xp := (*ifaceWords)(unsafe.Pointer(&x)) // New value 
  7.   for { 
  8.     typ := LoadPointer(&vp.typ) 
  9.     if typ == nil { 
  10.       // Attempt to start first store. 
  11.       // Disable preemption so that other goroutines can use 
  12.       // active spin wait to wait for completion; and so that 
  13.       // GC does not see the fake type accidentally. 
  14.       runtime_procPin() 
  15.       if !CompareAndSwapPointer(&vp.typ, nil, unsafe.Pointer(^uintptr(0))) { 
  16.         runtime_procUnpin() 
  17.         continue 
  18.       } 
  19.       // Complete first store. 
  20.       StorePointer(&vp.data, xp.data) 
  21.       StorePointer(&vp.typ, xp.typ) 
  22.       runtime_procUnpin() 
  23.       return 
  24.     } 
  25.     if uintptr(typ) == ^uintptr(0) { 
  26.       // First store in progress. Wait. 
  27.       // Since we disable preemption around the first store, 
  28.       // we can wait with active spinning. 
  29.       continue 
  30.     } 
  31.     // First store completed. Check type and overwrite data. 
  32.     if typ != xp.typ { 
  33.       panic("sync/atomic: store of inconsistently typed value into Value"
  34.     } 
  35.     StorePointer(&vp.data, xp.data) 
  36.     return 
  37.   } 

大概的逻辑:

  • 通过unsafe.Pointer将现有的和要写入的值分别转成ifaceWords类型,这样我们下一步就可以得到这两个interface{}的原始类型(typ)和真正的值(data)。
  • 开始就是一个无限 for 循环。配合CompareAndSwap使用,可以达到乐观锁的效果。
  • 通过LoadPointer这个原子操作拿到当前Value中存储的类型。下面根据这个类型的不同,分3种情况处理。

第一次写入 - 一个atomic.Value实例被初始化后,它的typ字段会被设置为指针的零值 nil,所以先判断如果typ是nil 那就证明这个Value实例还未被写入过数据。那之后就是一段初始写入的操作:

  • runtime_procPin()这是runtime中的一段函数,一方面它禁止了调度器对当前 goroutine 的抢占(preemption),使得它在执行当前逻辑的时候不被打断,以便可以尽快地完成工作,因为别人一直在等待它。另一方面,在禁止抢占期间,GC 线程也无法被启用,这样可以防止 GC 线程看到一个莫名其妙的指向^uintptr(0)的类型(这是赋值过程中的中间状态)。
  • 使用CAS操作,先尝试将typ设置为^uintptr(0)这个中间状态。如果失败,则证明已经有别的线程抢先完成了赋值操作,那它就解除抢占锁,然后重新回到 for 循环第一步。
  • 如果设置成功,那证明当前线程抢到了这个"乐观锁”,它可以安全的把v设为传入的新值了。注意,这里是先写data字段,然后再写typ字段。因为我们是以typ字段的值作为写入完成与否的判断依据的。

第一次写入还未完成- 如果看到typ字段还是^uintptr(0)这个中间类型,证明刚刚的第一次写入还没有完成,所以它会继续循环,一直等到第一次写入完成。

第一次写入已完成 - 首先检查上一次写入的类型与这一次要写入的类型是否一致,如果不一致则抛出异常。反之,则直接把这一次要写入的值写入到data字段。

这个逻辑的主要思想就是,为了完成多个字段的原子性写入,我们可以抓住其中的一个字段,以它的状态来标志整个原子写入的状态。

读取(Load)操作

先上代码:

  1. func (v *Value) Load() (x interface{}) { 
  2.   vp := (*ifaceWords)(unsafe.Pointer(v)) 
  3.   typ := LoadPointer(&vp.typ) 
  4.   if typ == nil || uintptr(typ) == ^uintptr(0) { 
  5.     // First store not yet completed. 
  6.     return nil 
  7.   } 
  8.   data := LoadPointer(&vp.data) 
  9.   xp := (*ifaceWords)(unsafe.Pointer(&x)) 
  10.   xp.typ = typ 
  11.   xp.data = data 
  12.   return 

读取相对就简单很多了,它有两个分支:

如果当前的typ是 nil 或者^uintptr(0),那就证明第一次写入还没有开始,或者还没完成,那就直接返回 nil (不对外暴露中间状态)。

否则,根据当前看到的typ和data构造出一个新的interface{}返回出去。

总结

本文由浅入深的介绍了atomic.Value的使用姿势,以及内部实现。让大家不仅知其然,还能知其所以然。

另外,原子操作由底层硬件支持,对于一个变量更新的保护,原子操作通常会更有效率,并且更能利用计算机多核的优势,如果要更新的是一个复合对象,则应当使用atomic.Value封装好的实现。

而我们做并发同步控制常用到的Mutex锁,则是由操作系统的调度器实现,锁应当用来保护一段逻辑。

 

责任编辑:武晓燕 来源: 网管叨bi叨
相关推荐

2020-06-12 10:03:01

线程安全多线程

2021-12-06 12:48:40

Gosyncatomic

2016-11-23 15:48:05

iOS APPCache

2023-03-02 08:19:43

不加锁程序实时性

2017-06-02 08:48:29

互斥锁JavaCAS

2018-06-14 14:02:30

苹果防水iPhone

2023-01-26 02:07:51

HashSet线程安全

2010-09-03 09:08:09

2024-01-19 08:42:45

Java线程字符串

2023-10-15 12:23:10

单线程Redis

2020-04-22 20:35:02

HashMap线程安全

2022-01-24 07:01:20

安全多线程版本

2022-02-16 11:56:28

HTTPHTTPS数据传输

2023-04-30 23:16:53

Go数组切片

2022-06-07 23:28:05

线程安全后端

2022-09-26 13:46:18

Java线程安全

2022-04-02 07:19:09

CORS前端安全

2024-03-22 12:29:03

HashMap线程

2020-10-30 16:20:38

Redis单线程高并发

2020-06-11 09:35:39

Redis单线程Java
点赞
收藏

51CTO技术栈公众号