经过 Once学习 Go 的内存模型golang
Once 官方描述 Once is an object that will perform exactly one action,即 Once 是一个对象,它提供了保证某个动做只被执行一次功能,最典型的场景就是单例模式。bash
package main
import (
"fmt"
"sync"
)
type Instance struct {
name string
}
func (i Instance) print() {
fmt.Println(i.name)
}
var instance Instance
func makeInstance() {
instance = Instance{"go"}
}
func main() {
var once sync.Once
once.Do(makeInstance)
instance.print()
}
复制代码
once.Do 中的函数只会执行一次,并保证 once.Do 返回时,传入Do的函数已经执行完成。(多个 goroutine 同时执行 once.Do 的时候,能够保证抢占到 once.Do 执行权的 goroutine 执行完 once.Do 后,其余 goroutine 才能获得返回 )函数
源码很简单,可是这么简单不到20行的代码确能学习到不少知识点,很是的强悍。学习
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()
}
}
复制代码
这里的几个重点知识:ui
先回答第一个问题?若是直接 o.done == 0,会致使没法及时观察 doSlow 对 o.done 的值设置。具体缘由能够参考 Go 的内存模型 ,文章中提到:atom
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 后能够及时的被取到。spa
再看第二个问题,能够直接使用 o.done == 0 是由于使用了 Mutex 进行了锁操做,o.done == 0 处于锁操做的临界区中,因此能够直接进行比较。指针
相信到这里,你就会问到第三个问题 atomic.StoreUint32(&o.done, 1) 也处于临界区,为何不直接经过 o.done = 1 进行赋值呢?这其实仍是和内存模式有关。Mutex 只能保证临界区内的操做是可观测的 即只有处于o.m.Lock() 和 defer o.m.Unlock()之间的代码对 o.done 的值是可观测的。那这是 Do 中对 o.done 访问就能够会出现观测不到的状况,所以须要使用 StoreUint32 保证原子性。code
到这里是否是发现了收获了好多,还有更厉害的。 咱们再看看为何 dong 不使用 uint8或者bool 而要使用 uint32呢?orm
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 (amd64/x86),
// and fewer instructions (to calculate offset) on other architectures.
done uint32
m Mutex
}
复制代码
目前能看到缘由是:atomic 包中没有提供 LoadUint8 、LoadBool 的操做。
而后看注释,咱们发现更为深奥的秘密:注释提到一个重要的概念 hot path,即 Do 方法的调用会是高频的,而每次调用访问 done,done位于结构体的第一个字段,能够经过结构体指针直接进行访问(访问其余的字段须要经过偏移量计算就慢了)