Go sync.Once的三重门
https://colobu.com/2021/05/05/triple-gates-of-sync-Once/
首先,第 15 行的 defer atomic.StoreUint32(&o.done, 1) 可以确保执行万第 16 行的 f 才将 done 设置为 1。
typo 执行万 => 执行完
麻烦问一下,为什么不用 atomic.CompareAndSwapUint32 方法,替代 atomic.LoadUint32、atomic.StoreUint32 和 Mutex?
好吧,刚看了一眼源码,源码里写的清清楚楚 https://go.googlesource.com/go/+/go1.16.3/src/sync/once.go#42
补一个文末点题:
- 第一重门:原子读
if atomic.LoadUint32(&o.done) == 0,只有f()未执行结束才需要sync.Mutex提供悲观锁的线程安全以及阻塞语义,初始化完成后无锁,性能高。 - 第二重门:上锁
o.m.Lock()利用线程同步机制确保并发安全,以及利用锁确保 happens before 的语义,弥补 Go 中原子操作没有 happens before 的缺陷 - 第三重门:double-check,避免阻塞于锁的协程被唤醒后重复执行
f()
有个问题,if atomic.LoadUint32(&o.done) == 0 能否替换成 if o.done == 0 ,这样虽然会多次进入doSlow, 但是后面大部分时候都不会进入的。这样直接读是否能比原子读提供更好的性能。
第六行不用 atomic 也能看到设置的结果呀
我觉得 defer atomic.StoreUint32(&o.done, 1) 是不是也可以替换成 o.done=1。mutex 已经可以保证不会重排,f 执行完再执行这一句了。所以修改成 o.done=1 我觉得也没问题。
这样的话,Go 可以保证第 15 行 defer atomic.StoreUint32(&o.done, 1) 肯定会在第 16 行 f() 之后执行,
这样就不会出现未初始化完成就将 done 设置为 1 的问题。
见原文第三节有解释。
我之前也是认为 StoreUint32 是为了其他地方能读到最新值,但是看起来只有 Do 开始那里(第六行)会读这个值,但是那里已经使用 LoadUint32 读取最新值了。
看了一下,StoreUint32 是为了避免函数 f 和设置 done = 1 的乱序执行。