0x00

今天看了这篇Go 标准库源码学习(一)详解短小精悍的 Once 之后,感觉对Go语言中的atomic理解又进一步,记录一下。

0x01 sync.Once

上面这篇文章集中讨论了sync.Once的实现,咱们直奔主题,先看下完整代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

type Once struct {
done uint32
m Mutex
}

func (o *Once) Do(f func()) {
if atomic.LoadUint32(&o.done) == 0 { // 位置1
o.doSlow(f)
}
}

func (o *Once) doSlow(f func()) {
o.m.Lock() // 位置2
defer o.m.Unlock()
if o.done == 0 { // 位置3
defer atomic.StoreUint32(&o.done, 1) // 位置4
f()
}
}

本文重点分析注释1,3,4三个地方对done读写的时候,为啥1和4需要用atomic, 而3这个地方却并不需要的问题。

0x02 内存模型

主要是由Go的内存模型 决定的.Go中不同goroutine之间,读写语义的可观测性需要一定的同步手段来保证。官博列出以下几种:

  • Initialization
  • Goroutine creation
  • Goroutine destruction
  • Channel communication
  • Locks
  • Once

没有提到atomic。但其实atomic也是能保证可观测性的,而且上面这几种手段也往往需要依赖atomic. atomic的代码,编译后的cpu指令具有LOCK前缀(x86), 说明是锁cacheline的,是Go里最强的内存模型

0x03 Lock与Unlock的happens-before

根据Go语言规范, Unlock()返回happens-before Lock()返回.
我们可以设想有AB两个gorotine同时执行Do, 在位置1处检测的时候条件都为真,二者都进入doSlow.不过两者总有一个先那到锁,我门假设是A, 那么B此时便阻塞在位置2(Lock)这个地方了.

A执行完f之后,调用defer Unlock, 因为Unlock happens-before Lock, 所以位置4这个地方的写操作,当goroutine BLock返回,执行到位置3的时候y一定可以观测到goroutine A之前在位置4的写操作,即使位置4不是atomic也可以。

可见,由于Lock提供的happens-before语义保证,位置3这个地方的读操作是不需要用到atomic

0x04 atomic

位置1位置4这两个个地方的atomic存在理由又是什么呢?

考虑这样一种情况, goroutine A刚执行完位置4处的写操作,goroutine B还被阻塞在Lock调用上,这时又有goroutine C再次进入位置1

假设位置1位置4没有用atomic,而是普通读写。那么因为不同goroutine间对同一变量不存在同步手段,Go语言并不保证goroutine A的写操作能被goroutine C读到,导致goroutine C继续进入慢路径,这不符合我们的要求。

所以,位置1位置4atomic是为了解决gorouine Agoroutine C这种情况下的可观测性的。

0x05 小结

本文详细考察了sync.Once的实现中,几处读写对于是否使用atomic时的考虑,加深了Go语言内存模型的理解。

0x06 参考资料

本文完。