sync.Once 用于保证一段任务执行 exactly once。它的实现非常简单:
type Once struct {
done uint32
m Mutex
}
func (o *Once) Do(f func()) {
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()
}
}
可以看出 Once 的工作流程:
- 通过 atomic 来检查 done 判断是否已经被执行了
- 通过 Mutex 保证同一时刻只有一个协程进入执行
- 检查一遍 done 是否被改写
- 执行并更新 done 值
整个流程其实就是双检锁单例的实现,在锁的基础上引入了 double check,所以有下面几个点值得注意:
- 为什么要设置多次的检查?
观察一下 doSlow ,这本身就是一个完整且可以独立的工作的流程,但是它的问题在于它太慢了,每一次检查都需要竞争锁。所以为了提高性能,通过前置检查 atomic 来快速判断是否已经处理完成。
- 既然前面已经检查过 done 的值了,为什么在获取到锁执行前需要再检查一遍?
因为整个流程并非原子操作,在等待互斥锁的时候,前面的协程可能已经改写了 done 值,后面再放行进入的协程需要保证 done 没有被改写过。