golang atomic.Value实践以及原理

Posted 惜暮

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了golang atomic.Value实践以及原理相关的知识,希望对你有一定的参考价值。

golang atomic.Value实践以及原理

最近一个项目中某个对象需要持有一个统计对象,并且需要能够原子的更新这个统计对象(并发读写场景下),避免data race。为了避免对象的copy, 肯定是要持有这个统计对象的指针了。 此外,这个统计对象其实是统计的模型,需要能够随时替换成其余的统计实现。所以很自然的选用了 interface来保存。

type Obj struct 
	data interface

但是有个问题,golang 里面的atomic没有提供方法来实现interface的原子读写。但是atomic提供了 atomic.Value. 该类型可以原子更新 interface。

数据结构

先看数据结构的定义:

// 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


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

atomic.Value 里面其实维护的就是一个 interface. 然后提供原子的更新这个interface的方法。

注意注释所说的,提供的是原子的Store 和 load 一个类型一致的value。也就是说 atomic.Value 一旦第一次Store了一个value, 那么后面的Store就必须是同一个类型的value。然后就是在store之后,Value不能被复制。

注意到下面还有一个结构:ifaceWords ,这个其实是对 interface 内部结构的表示。我们知道一个interface会被编译编译成 eface 结构或则 iface 结构。定义在 runtime/runtime2.go 里面。eface表示一个空接口,iface描述的是非空接口,它包含方法。

type iface struct 
	tab  *itab
	data unsafe.Pointer


type eface struct 
	_type *_type
	data  unsafe.Pointer

不管是 iface 还是 eface 里面保存的都是两个指针对象。所以我们可以把 interface 对象的指针转换成*ifaceWords,这与后面的Store 和 Load息息相关。

关于非类型安全的指针转换unsafe.Pointer,可以参考这篇文章:golang非类型安全的指针以及指针转换。

Store

// Store sets the value of the Value to x.
// All calls to Store for a given Value must use values of the same concrete type.
// Store of an inconsistent type panics, as does Store(nil).
func (v *Value) Store(x interface) 
	if x == nil 
		panic("sync/atomic: store of nil value into Value")
	
	// 转换*Value 为 *ifaceWords
	vp := (*ifaceWords)(unsafe.Pointer(v))
	// 转换要存储的value为(*ifaceWords)
	xp := (*ifaceWords)(unsafe.Pointer(&x))
	for 
		// 原子加载atomic.Value里面当前存储的变量类型
		typ := LoadPointer(&vp.typ)
		// type为空,表示第一次加载
		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.
			// 当前线程禁止抢占,GC也不会看到这个中间态
			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
		
		// 正在第一次Store的中间过程中(也就是中间态)
		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
	

首先从注释里面就可以得到两个关键信息:

  1. 如果Store的value的类型和第一次Store的类型不一致会直接panic
  2. 如果Store nil也会直接panic。

现在描述一下大致流程:

  1. 先把现有值和将要写入的值转换成ifaceWords类型,这样我们下一步就可以得到这两个interface的原始类型(typ)和真正的值(data)。
  2. 进入 一个无限 for 循环。配合CompareAndSwap食用,可以达到乐观锁的功效。
  3. 通过LoadPointer这个原子操作拿到当前Value中存储的类型。下面根据这个类型的不同,分3种情况处理:
    1. 一个Value实例被初始化后,它的typ字段会被设置为指针的零值 nil,所以先判断如果typ是 nil 那就证明这个Value还未被写入过数据。那之后就是一段初始写入的操作:
      1. runtime_procPin(),它可以将一个goroutine死死占用当前使用的P(P-M-G中的processor),不允许其它goroutine/M抢占, 使得它在执行当前逻辑的时候不被打断,以便可以尽快地完成工作,因为别人一直在等待它。另一方面,在禁止抢占期间,GC 线程也无法被启用,这样可以防止 GC 线程看到一个莫名其妙的指向^uintptr(0)的类型(这是赋值过程中的中间状态)。
      2. 使用CAS操作,先尝试将typ设置为^uintptr(0)这个中间状态。如果失败,则证明已经有别的线程抢先完成了赋值操作,那它就解除抢占锁,然后重新回到 for 循环第一步。
      3. 如果设置成功,那证明当前线程抢到了这个"乐观锁”,它可以安全的把v设为传入的新值了(19~23行)。注意,这里是先写data字段,然后再写typ字段。因为我们是以typ字段的值作为写入完成与否的判断依据的。
    2. 第一次写入还未完成,如果看到 typ字段还是^uintptr(0)这个中间类型,证明刚刚的第一次写入还没有完成,所以它会继续循环,“忙等"到第一次写入完成。
    3. 第一次写入已完成(第31行及之后) - 首先检查上一次写入的类型与这一次要写入的类型是否一致,如果不一致则抛出异常。反之,则直接把这一次要写入的值写入到data字段。

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

Load

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返回出去。

一些总结

原子操作由底层硬件支持,而锁则由操作系统提供的 API 实现。若实现相同的功能,前者通常会更有效率,并且更能利用计算机多核的优势。所以,以后当我们想并发安全的更新一些变量的时候,我们应该优先选择用atomic.Value来实现。

使用规则:

  1. 不能用atomic.Value原子值存储nil
  2. 我们向原子值存储的第一个值,决定了它今后能且只能存储哪一个类型的值

建议:不要把内部使用的atomic.Value原子值暴露给外界,如果非要暴露也要通过API封装形式,做严格的check。

以上是关于golang atomic.Value实践以及原理的主要内容,如果未能解决你的问题,请参考以下文章

golang sync.Map 原理以及性能分析

Golang 高效实践之并发实践context篇

Golang gRPC 实践

干货|golang实现非对称加密技术实践

https原理以及golang基本实现

golang在阿里开源容器项目Pouch中的应用实践