sync.Once 源码
package sync
import (
"sync/atomic"
)
type Once struct {
done uint32
m Mutex
}
func (o *Once) Do(f func()) {
if atomic.LoadUint32(&o.done) == 0 {
// Outlined slow-path to allow inlining of the fast-path.
o.doSlow(f)
}
}
func (o *Once) doSlow(f func()) {
o.m.Lock()
defer o.m.Unlock()
if o.done == 0 {
defer atomic.StoreUint32(&o.done, 1)
f()
}
}
剔除源码注释之后,才这么几行代码,却能发挥巨大的作用,但里面有些小细节还是值得好好推敲的。
适合场景:
- 全局变量初始化
- 懒汉模式的单例
- 服务接受系统级别的kill信号去触发业务代码,比如 kill -15 pid。
- 所有只允许执行一次的场景。。
不太适合场景:
- 一上来就高并发场景:一上来就高并发场景看源码就会发现逻辑走到 sync.Mutex 里面去控制了,那么这个时候,你倒是不如直接 sync.Mutex 去控制。 不过也就多了个 atomic.LoadUint32 级别的操作呀,所以不足为虑。
细节刨析
image.png以下纯搬运,其实源码注释里面也有
答:如果不使用 atomic,无法及时观察doSlow对o.done 的修改。 什么叫及时观察? 首先 要明白
sync.Once{}.Do()
它是可能被高频的在多核里面并发并行运行的,内存同步可能是不及时的。假如将 atomic 的操作改为普通的
&o.done==0 与 o.done = 1
语句其实也是会起到内存同步的(go里面内存模型一致性的保障是可以通过同步事件去达到的,这里o.m.lock就是同步事件之一),但这个同步事件是发生在f() 函数执行完毕后去同步的(而且还真得等到f执行完毕,后面说..)假如f执行花了5ms,就意味着5ms放进来的并发请求串行全靠 o.m.lock 去保证了。加不加 atomic 都会有lock给你兜底,只是atomic加了 done一旦变化,里面上层的 if atomic.LoadUint32(&o.done) == 0 {
就判断出来了,而不加还需要unlock触发同步事件,很明显前者更及时。
为啥非要等f() 执行完毕 再通过 o.m.Unlock() 去触发同步事件,达到内存一致性,为啥不把f() 放goroutinue 直接执行 从而更快的触发 同步事件,这样上文的5ms不就不存在了么。
答:其实这样首先你也只能是缩短5ms这个时间,其次还真不能不等f() 没执行完就触发修改done的值并里面触发同步事件,你想撒,f() 明明还没执行完,你就通知其他人 这个函数已经执行过了,这是不对的,万一后面的业务对 f() 执行结果有强依赖。。。 伪高效埋巨坑。 其实这也是为什么没有用 if atomic.CompareAndSwapUint32(&o.done, 0, 1) {
去替换 atomic.LoadUint32(&o.done) == 0 {
的原因了
网友评论