单例实现
type singleton struct{}
var (
instance *singleton
initialized uint32
mu sync.Mutex
)
func Instance() *singleton {
if atomic.LoadUint32(&initialized) == 1 {
return instance
}
mu.Lock()
defer mu.Unlock()
if instance == nil {
defer atomic.StoreUint32(&initialized, 1)
instance = &singleton{}
}
return instance
}
其中通用的代码提取出来,就成了标准库中sync.Once
的实现:
type Once struct {
done uint32
m sync.Mutex
}
func (o *Once) Do(f func()) {
if atomic.LoadUint32(&o.done) == 0 {
o.m.Lock()
defer o.m.Unlock()
if o.done == 0 {
defer atomic.StoreUint32(&o.done, 1)
f()
}
}
}
于是,使用sync.Once
重新实现单例模式
var (
instance2 *singleton
once sync.Once
)
func Instance2() *singleton {
once.Do(func() {
instance2 = &singleton{}
})
return instance2
}
sync.Once
源码分析
1. lock并不会同步值
在lock和unlock之间修改值,并不会保证对其他协程是可见的,除非使用相同的Mutex加锁,想要同步值必须使用atomic;
lock可以通过串行化,使得两个协程的操作存在happen-before
关系,从而是的操作可见
happen-before
原则定义如下:
如果一个操作happens-before(之前发生)另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则制定的顺序来执行。如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法。
2. Do
执行一次
当第一次执行完Do
之后,done
设置成1,后面执行Do
会直接跳过
3. Once
执行Do
后不准copy
A Once must not be copied after first use.
sync.Once
执行完Do
后done
已经设置成1了,copy出来的once执行Do
会直接跳过
4. Do
并发时阻塞
当两个或者多个协程同时调用Do
时,先调用的协程执行,后面的协程会阻塞;
解释:以单例使用once的实现说明,两个协程同时调用Instance2()
,先调用的协程执行创建并拿到返回值,后调用的阻塞,
等到先调用的完成后再拿到返回值;
意义:这样的好处是防止后调用的协程拿到的是nil
源码说明:上面第二段代码13行使用defer
,要等f()
结束才会把done
设置成1;其他协程并发调用Do
时,done==0
,
然后请求`m.Lock()`形成阻塞
5. Do
递归死锁
如果Do
中的方法调用当前once的Do
会造成死锁,原因参考上面一点(sync.Mutex.Lock()
时不可重入锁)
参考
- 《Go语言高级编程》
- Go1.16源码