sync.once 解析
Once 描述是 Once is an object that will perform exactly one action,即 Once 是一个只执行一次的对象。
使用场景
当我们需要在多个 goroutine 中只执行一次初始化操作时,可以使用 Once 来完成。
先举一个官网给出的例子:
import (
"fmt"
"sync"
)
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
}
}
在这个例子中,我们创建了 10 个 goroutine,但是 onceBody 只执行了一次。即只会打印一次 Only once。
Once 的 Do 方法接收一个函数类型的参数,这个函数类型的参数就是需要保证只执行一次的那段代码。Once 会通过互斥锁和布尔值来保证只执行一次。
Once 经常被用来做单例模式,因为单例模式需要保证只执行一次初始化操作。
这里也给出一个单例模式的例子:
type singleton struct{}
var instance *singleton
var once sync.Once
func GetInstance() *singleton {
once.Do(func() {
instance = &singleton{}
})
return instance
}
而 Once 还可以用来做等待事件的触发器,比如下面这个例子:
func main() {
var once sync.Once
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
once.Do(func() {
fmt.Println("Print 1")
})
}()
go func() {
defer wg.Done()
once.Do(func() {
fmt.Println("Print 2")
})
}()
wg.Wait()
}
在这个例子中,我们使用 Once 来实现了一个等待事件的触发器,即两个 goroutine 都会等待 once 的触发,而 once 只会触发一次。
Once 的实现原理
Once 的实现原理是通过一个 Mutex 和一个布尔值来实现的,布尔值记录是否已经执行过,Mutex 用来保证并发安全。
看一下 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()
}
}
-
为什么要使用
atomic.LoadUint32(&o.done) == 0而不使用o.done == 0呢?如果直接使用
o.done == 0,会无法及时观察doSlow中对o.done的修改,从而导致可能的多次执行f()。这是与 go 语言的内存模型有关的。只有用保证并发安全的方式读取o.done,才能保证读取到的是最新的值。而下面的atomic.StoreUint32(&o.done, 1)也是同样的道理。 -
doSlow是什么意思?doSlow方法的存在主要是为了性能优化。将慢路径(slow-path)代码从 Do 方法中分离出来,使得 Do 方法的快路径(fast-path)能够被内联,从而提高性能。 -
为什么需要两次检查
done的值在
doSlow内部检查是为了避免多次执行f(),因为在o.done == 0为真时,可能已经有多于一个的 goroutine 进入了doSlow,多个 goroutine 在等待第一个获取到锁的 goroutine 释放锁让他们好执行函数,但是我们需要保证只有一次的执行,因此其他 goroutine 获取到锁后需要再次检查done的值,如果已经执行过f(),则不再执行。从而保证在doSlow的defer atomic.StoreUint32(&o.done, 1)执行后,其他 goroutine 获取到锁后不会再执行f()。
Do 方法不提供返回值,因此如果有需要在初次执行后有 error 的情况下继续执行其他调用 Once 直到成功的需求,可以写一个自己的 Once,比如下面这个例子:
type Once struct {
done uint32
m Mutex
}
func (o *Once) Do(f func() error) error {
if atomic.LoadUint32(&o.done) == 0 {
// Outlined slow-path to allow inlining of the fast-path.
return o.doSlow(f)
}
return nil
}
func (o *Once) doSlow(f func() error) error {
o.m.Lock()
defer o.m.Unlock()
if o.done == 0 {
err := f()
if err == nil{
atomic.StoreUint32(&o.done, 1)
}
}
return nil
}
死锁
常见的像这样的死锁:
func main() {
var onceA, onceB sync.Once
var initB func()
initA := func() { onceB.Do(initB) }
initB = func() { onceA.Do(initA) }
onceA.Do(initA)
}
还有一个死锁的例子,就是在 Do 里面调用 Do,比如下面这个例子:
func main() {
var once sync.Once
once.Do(func() {
once.Do(func() {
fmt.Println("hello")
})
})
}
内部的 once.Do 会等待外部 once.Do 的执行结果,但是外部 once.Do 的执行结果需要等待内部 once.Do 的执行结果,从而导致死锁。
总结
sync.Once 是 Go 语言标准库中的一个同步工具,用于确保在多个 Goroutine 中某个函数只会被执行一次。
- 确保函数只执行一次:sync.Once 通过一个内部的标记位来保证其中的函数只会被执行一次,后续调用会被阻塞,直到第一次调用完成。
- 安全地并发执行:多个 Goroutine 并发调用 sync.Once.Do(f),其中 f 是需要确保只执行一次的函数,但只有一个 Goroutine 实际上会执行函数 f,其他 Goroutine 会被阻塞。
- 零值安全:sync.Once 的零值为 nil,可以直接使用而无需初始化。
- 灵活性:sync.Once 可以用于单例模式、延迟初始化等场景。