sync:二. 延迟初始化(once)
sync.Once 是 Go 标准库提供的使函数只执行一次的实现。作用与 init 函数类似,但有区别。在某些情况下预先初始化一个变量会增加函数的启动延迟,如果实际执行时可能用不上这个变量,那么初始化就是非必须的。sync.Once很好的解决了这个问题,Once可以在任意的位置调用,并且只会执行一次,并发场景下是线程安全的。
Do方法
Once只提供了一个 func (o *Once) Do(f func()) 方法。Do方法调用一个函数,这里有三种情况:
- 如果函数没有被调用则调用它。
- 如果函数已经被调用完成则不调用.
- 如果函数正在被调用(未完成),则等待函数调用完成再返回。
func main() {
var once sync.Once
onceBody := func(){
fmt.Println("Only once")
}
done := make(chan bool)
for i:=0; i<10; i++ {
go func() {
once.Do(onceBody)
done <- true
}()
}
for i:=0; i<10; i++ {
<-done
}
}
// 结果:
// Only once
Once源码
Once结构
// Once 是一个将执行一个动作的对象。
type Once struct {
// done 指示操作是否已执行。它在结构中是第一个,因为它用于热路径。热路径在每个调用站点内联。
// 首先放置允许在某些架构(amd64x86)上使用更紧凑的指令,而在其他架构上使用更少的指令(计算偏移量)。
done uint32
m Mutex
}
Once结构由两个字段一个 uint32类型的done来标志操作是否执行。这个字段放在第一个的原因是可以使用偏移量的形式来获取。然后就是一个互斥锁。
Do实现
func (o *Once) Do(f func()) {
// 注意:这是 Do 的错误实现: if atomic.CompareAndSwapUint32(&o.done, 0, 1) { f() }
// Do 保证当它返回时, f 已经完成。这个实现不会实现这个保证:
// 给定两个同时调用,cas 的获胜者将调用 f,第二个将立即返回,
// 而不等待第一个对 f 的调用完成。这就是为什么慢速路径回退到互斥体,
// 以及为什么 atomic.StoreUint32 必须延迟到 f 返回之后。
if atomic.LoadUint32(&o.done) == 0 {
// 慢速路径以允许内联快速路径。
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()
}
}
// 快速路径是指做功较少的路径,而慢速路径是指做功较多的路径。
这里可以看到如果第二次调用函数时发现第一次调用还未调用结束,会在doSlow中等待解锁,这里用一个互斥锁避免第一次调用未结束,第二次调用就返回的情况。