sync.Once 是 Go 标准库提供的函数,可以用于实现单例模式,确保回调函数只执行一次,那么它是怎么实现的呢?
快速入门
首先来了解下如何使用 sync.Once,它的使用方法很简单,如下
func main() {
var once sync.Once
onceFunc := func() {
fmt.Println("sync once")
}
for i := 0; i < 10; i++ {
go func() {
once.Do(onceFunc)
}()
}
time.Sleep(time.Second)
}
// Output
// sync once
可以看到只打印了一次输出
源码阅读
那么接下来我们通过源码来了解下 sync.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()
}
}
sync.Once 的源码实现非常简单,采用的是双重检测锁机制 (Double-checked Locking),是并发场景下懒汉式单例模式的一种实现方式
- 首先判断 done 是否等于 0,等于 0 则表示回调函数还未被执行
- 加锁,确保并发安全
- 在执行函数前,二次确认 done 是否等于 0,等于 0 则执行
- 将 done 置 1,同时释放锁
疑问一: 为什么不使用乐观锁 CAS
其实不使用乐观锁 CAS 的原因,这个在 sync.Once 的源码注释中已经明确说明了
// 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.
简单的来说就是 f() 的执行结果最终可能是不成功的,所以你会看到现在采用的是双重检测锁机制来实现,同时需要等 f() 执行完成才修改 done 值
疑问二: 为什么读取 done 值的方式没有统一
比较 done 是否等于 0,为什么有的地方用的是 atomic.LoadUint32,有的地方用的却是 o.done。主要原因是 atomic.LoadUint32 可以保证原子读取到 done 值,是并发安全的,而在 doSlow 中,已经加锁了,那么临界区就是并发安全的,使用 o.done 就可以来读取值就可以了