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 参考资料
本文完。