Go标准库sync.Once详解
0x00
今天看了这篇Go 标准库源码学习(一)详解短小精悍的 Once 之后,感觉对Go语言中的atomic理解又进一步,记录一下。
0x01 sync.Once
上面这篇文章集中讨论了sync.Once的实现,咱们直奔主题,先看下完整代码:
1 |
|
本文重点分析注释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()返回.
我们可以设想有A和B两个gorotine同时执行Do, 在位置1处检测的时候条件都为真,二者都进入doSlow.不过两者总有一个先那到锁,我门假设是A, 那么B此时便阻塞在位置2(Lock)这个地方了.
等A执行完f之后,调用defer Unlock, 因为Unlock happens-before Lock, 所以位置4这个地方的写操作,当goroutine B 从Lock返回,执行到位置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和位置4的atomic是为了解决gorouine A和goroutine C这种情况下的可观测性的。
0x05 小结
本文详细考察了sync.Once的实现中,几处读写对于是否使用atomic时的考虑,加深了Go语言内存模型的理解。
0x06 参考资料
本文完。