Go單例模式與Once源碼實現
單例實現
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(之前發(fā)生)另一個操作,那么第一個操作的執(zhí)行結果將對第二個操作可見,而且第一個操作的執(zhí)行順序排在第二個操作之前。
兩個操作之間存在happens-before關系,并不意味著一定要按照happens-before原則制定的順序來執(zhí)行。如果重排序之后的執(zhí)行結果與按照happens-before關系來執(zhí)行的結果一致,那么這種重排序并不非法。
2. Do執(zhí)行一次
當第一次執(zhí)行完Do
之后,done
設置成1,后面執(zhí)行Do
會直接跳過
3. Once執(zhí)行Do后不準copy
A Once must not be copied after first use.
sync.Once
執(zhí)行完Do
后done
已經設置成1了,copy出來的once執(zhí)行Do
會直接跳過
4. Do并發(fā)時阻塞
當兩個或者多個協程同時調用Do
時,先調用的協程執(zhí)行,后面的協程會阻塞;
解釋:以單例使用once的實現說明,兩個協程同時調用Instance2()
,先調用的協程執(zhí)行創(chuàng)建并拿到返回值,后調用的阻塞,
? 等到先調用的完成后再拿到返回值;
意義:這樣的好處是防止后調用的協程拿到的是nil
源碼說明:上面第二段代碼13行使用defer
,要等f()
結束才會把done
設置成1;其他協程并發(fā)調用Do
時,done==0
,
? 然后請求m.Lock()
形成阻塞
5. Do遞歸死鎖
如果Do
中的方法調用當前once的Do
會造成死鎖,原因參考上面一點(sync.Mutex.Lock()
時不可重入鎖)
參考
- 《Go語言高級編程》
- Go1.16源碼