golang原子操作

原子操作可以完成地消除竟态条件,并能够绝对保证并发安全性。并且,它地执行速度要比其他的同步工具快得多,通常会高出好几个数量级。

学习golang笔记, 整理来自郝大的课程:Go语言核心36讲

原子操作(atomic operation)

原子操作可以完成地消除竟态条件,并能够绝对保证并发安全性。并且,它地执行速度要比其他的同步工具快得多,通常会高出好几个数量级。

不过,它的缺点也很明显。

更具体地说,正是因为原子操作不能被中断,所以它需要足够简单,并且要求快速

操作系统层面只针对二进制或整数的原子操作提供了支持。

sync/atomic包中提供了几种原子操作?可操作的数据类型又有哪些?

golang中原子操作如下:

  • 加法(add)

  • 比较并交换(CAS,compare and swap)

  • 加载(load)

  • 存储(store)

  • 交换(swap)

数据类型有: int32、int64、uint32、uint64、uintptr以及unsafe包中的Pointer

unsafe.Pointer

1
2
3
4
bytes := []byte{104, 101, 108, 108, 111}
p := unsafe.Pointer(&bytes)
str := (*string)(p)
fmt.Println(str, *str)

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

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

比如说,[]bytestring其实内部的存储结构都是一样的,但 Go 语言的类型系统禁止他俩互换。如果借助unsafe.Pointer,我们就可以实现在零拷贝的情况下,将[]byte数组直接转换成string类型。

atomic.Value

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

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

写入操作(Store)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// 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{}) {
// 存储的值不能是nil
if x == nil {
panic("sync/atomic: store of nil value into Value")
}

// old value
vp := (*ifaceWords)(unsafe.Pointer(v))
// new value
xp := (*ifaceWords)(unsafe.Pointer(&x))
for {
// 通过原子操作获取当前Value中存储的类型
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.
// 如果typ是nil,那么这是第一次store
// 禁止运行时抢占,其他goroutine可以进行自旋,直到第一次写入成功
// runtime_procPin(),它可以将一个goroutine死死占用当前使用的P(P-M-G中的processor),不允许其它goroutine/M抢占,
// 使得它在执行当前逻辑的时候不被打断,以便可以尽快地完成工作,因为别人一直在等待它。
// 另一方面,在禁止抢占期间,GC 线程也无法被启用,这样可以防止 GC 线程看到一个莫名其妙的指向^uintptr(0)的类型(这是赋值过程中的中间状态)。
runtime_procPin()
// 使用CAS操作,原子性设置typ为^uintptr(0)这个中间状态。
// 如果失败,则证明已经有别的线程抢先完成了赋值操作,那它就解除抢占锁,然后重新回到 for 循环第一步。
if !CompareAndSwapPointer(&vp.typ, nil, unsafe.Pointer(^uintptr(0))) {
runtime_procUnpin()
continue
}
// Complete first store.
// 如果CAS设置成功,证明当前goroutine获取到了这个"乐观锁",可以安全地把v设为传入的新值
StorePointer(&vp.data, xp.data)
StorePointer(&vp.typ, xp.typ)
runtime_procUnpin()
return
}

// 写入进行中...
// 如果看到typ字段还是^uintptr(0)这个中间类型,证明刚刚的第一次写入还没有完成,
// 所以它会继续循环,“忙等"到第一次写入完成。
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")
}

// 直接把这一次要写入的值写入到data字段
StorePointer(&vp.data, xp.data)
return
}
}

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

流程图:

读取操作(Load)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Load returns the value set by the most recent Store.
// It returns nil if there has been no call to Store for this Value.
func (v *Value) Load() (x interface{}) {
vp := (*ifaceWords)(unsafe.Pointer(v))
typ := LoadPointer(&vp.typ)
// 如果typ为nil或者中间类型^uintptr(0),那么说明第一次写入还没有完成,那就直接返回nil
if typ == nil || uintptr(typ) == ^uintptr(0) {
// First store not yet completed.
return nil
}

// 到这里,说明第一次写入已成功
// 根据已有到typ和data,构建一个interface返回
data := LoadPointer(&vp.data)
xp := (*ifaceWords)(unsafe.Pointer(&x))
xp.typ = typ
xp.data = data
return
}

总结

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

使用规则:

  • 不能用atomic.Value原子值存储nil

  • 我们向原子值存储的第一个值,决定了它今后能且只能存储哪一个类型的值

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