Go单例模式与Once源码实现
作者:如雨随行2020
单例实现
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源码