简单介绍
sync.Once
是 Go 语言标准库 sync
中提供的一个并发原语,用于确保某个函数只会被执行一次,无论有多少个 Goroutine 尝试调用它。
这在 单例模式,资源清理 等场景中非常有用,因为它可以保证代码只执行一次,避免并发时的竞态条件。
基本使用
这里给出一个 使用 Once
实现懒汉模式的例子:
- 单例类声明为不可导出类型
worker
,避免被外界直接获取到; - 声明一个全局单例变量
var W iWorker
,但不进行初始化,留给给外部调用; - 暴露一个对外公开的方法
GetWorker()
,用于获取这个单例,并且在这个方法被调用时,判断单例是否初始化,倘若没有,则在此时完成初始化工作;
var (
once sync.Once
W iWorker
)
type iWorker interface {
Work()
}
type worker struct {
}
func (w *worker) Work() {
fmt.Println("I am working")
}
func newWorker() *worker {
return &worker{}
}
func GetWorker() iWorker {
once.Do(func() {
W = newWorker()
})
return W
}
源码分析
sync.Once
是 Golang 提供的用于支持实现单例模式的标准库工具,其对应的数据结构源码截图如下:
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/386),
// and fewer instructions (to calculate offset) on other architectures.
done uint32
m Mutex
}
简单的理解一下,其中 done
是一个整型变量,用来标识 once
保护的函数是否已经被执行过,m
是一个互斥锁,保证并发场景下的安全性。
Once
对外只提供一个方法,也就是 once.Do()
,本质上也是通过 double check
机制,解决并发不安全问题
func (o *Once) Do(f func()) {
// Note: Here is an incorrect implementation of Do:
//
// if atomic.CompareAndSwapUint32(&o.done, 0, 1) {
// f()
// }
//
// Do guarantees that when it returns, f has finished.
// This implementation would not implement that guarantee:
// given two simultaneous calls, the winner of the cas would
// call f, and the second would return immediately, without
// waiting for the first's call to f to complete.
// This is why the slow path falls back to a mutex, and why
// the atomic.StoreUint32 must be delayed until after f returns.
if atomic.LoadUint32(&o.done) == 0 {
// Outlined slow-path to allow inlining of the fast-path.
o.doSlow(f)
}
}
它会先执行一次原子操作,来检查 done
的值是否是 0,如果是 0,则说明该任务没有被执行过,则进一步调用 doSlow()
这个方法执行该任务。
同时,这里解释了一下为什么不能用 atomic.CompareAndSwapUint32(&o.done, 0, 1)
这个操作:假定两个同时调用,那么 CAS
的获胜者将调用 f()
, 而另一个将立即返回,无需等待第一个对 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()
中,会先加锁,二次检查 Once.done
的值,然后执行方法入参传入的闭包任务函数 f()
,最后通过 defer
将 Once.done
标记为 1, 保证全局只执行一次闭包任务函数。
最后结合之前给出的例子,梳理一下函数的执行流程
注意事项
不要在 Do
的 f()
中嵌套调用 Do
,因为 sync.Mutex
是一个不可重入锁,第二个 Do
方法会一直等待 doSlow()
中锁的释放导致发生了死锁,如下示例
func TestOnce(t *testing.T) {
o := &sync.Once{}
o.Do(func() {
o.Do(func() {
t.Log("do------>")
})
})
}
// fatal error: all goroutines are asleep - deadlock!