行业动态

通过 sync.Once 学习到 Go 的内存模型

经过 Once学习 Go 的内存模型

Once 官方描绘 Once is an object that will perform exactly one action,即 Once 是一个目标,它供给了确保某个动作只被履行一次功用,最典型的场景便是单例形式。

package main
import  print {
 fmt.Println
var instance Instance
func makeInstance {
 instance = Instance{ go }
func main {
 var once sync.Once
 once.Do
 instance.print
仿制代码

once.Do 中的函数只会履行一次,并确保 once.Do 回来时,传入Do的函数现已履行完结。

源码很简略,可是这么简略不到20行的代码确能学习到许多常识点,十分的强悍。

package sync
import  Do) {
 if atomic.LoadUint32 == 0 {
 // Outlined slow-path to allow inlining of the fast-path.
 o.doSlow
func  doSlow) {
 o.m.Lock
 defer o.m.Unlock
 if o.done == 0 {
 defer atomic.StoreUint32
仿制代码

这儿的几个要点常识:

先答复第一个问题?假如直接 o.done == 0,会导致无法及时调查 doSlow 对 o.done 的值设置。详细原因能够参阅 Go 的内存模型 ,文章中说到:

Programs that modify data being simultaneously accessed by multiple goroutines must serialize such access.
To serialize access, protect the data with channel operations or other synchronization primitives such as those in the sync and sync/atomic packages.
仿制代码

粗心是 当一个变量被多个 gorouting 拜访的时分,必需求确保他们是有序的,能够运用 sync 或许 sync/atomic 包来完成。用了 LoadUint32 能够确保 doSlow 设置 o.done 后能够及时的被取到。

再看第二个问题,能够直接运用 o.done == 0 是因为运用了 Mutex 进行了锁操作,o.done == 0 处于锁操作的临界区中,所以能够直接进行比较。

相信到这儿,你就会问到第三个问题 atomic.StoreUint32 也处于临界区,为什么不直接经过 o.done = 1 进行赋值呢?这其实仍是和内存形式有关。Mutex 只能确保临界区内的操作是可观测的 即只要处于o.m.Lock 和 defer o.m.Unlock之间的代码对 o.done 的值是可观测的。那这是 Do 中对 o.done 拜访就能够会呈现观测不到的状况,因而需求运用 StoreUint32 确保原子性。

到这儿是不是发现了收成了很多,还有更凶猛的。 咱们再看看为什么 dong 不运用 uint8或许bool 而要运用 uint32呢?

type Once struct {
 // done indicates whether the action has been performed.
 // It is first in the struct because it is used in the hot path.
 // The hot path is inlined at every call site.
 // Placing done first allows more compact instructions on some architectures ,
 // and fewer instructions  on other architectures.
 done uint32
 m Mutex
仿制代码

现在能看到原因是:atomic 包中没有供给 LoadUint8 、LoadBool 的操作。

然后看注释,咱们发现更为艰深的隐秘:注释说到一个重要的概念 hot path,即 Do 办法的调用会是高频的,而每次调用拜访 done,done坐落结构体的第一个字段,能够经过结构体指针直接进行拜访