深入理解 sync.Once:單例模式的絕佳選擇
sync.Once是讓函數方法只被調用執(zhí)行一次的實現,其最常應用于單例模式之下,例如初始化系統(tǒng)配置、保持數據庫唯一連接等。
sync.Once的單例模式示例
1package?main
2
3import?(
4????"fmt"
5????"sync"
6)
7
8type?Instance?struct{}
9
10var?(
11????once?????sync.Once
12????instance?*Instance
13)
14
15func?NewInstance()?*Instance?{
16????once.Do(func()?{
17????????instance?=?&Instance{}
18????????fmt.Println("Inside")
19????})
20????fmt.Println("Outside")
21????return?instance
22}
23
24func?main()?{
25????for?i?:=?0;?i?3;?i++?{
26????????_?=?NewInstance()
27????}
28}輸出
1$?go?run?main.go?
2Inside
3Outside
4Outside
5Outside
從上述例子可以看到,雖然多次調用NewInstance()函數,但是Once.Do()中的方法有且僅被執(zhí)行了一次。那么sync.Once是如何做到這一點的呢?
sync.Once的源碼解析
1type?Once?struct?{
2????//?done?indicates?whether?the?action?has?been?performed.
3????//?It?is?first?in?the?struct?because?it?is?used?in?the?hot?path.
4????//?The?hot?path?is?inlined?at?every?call?site.
5????//?Placing?done?first?allows?more?compact?instructions?on?some?architectures?(amd64/x86),
6????//?and?fewer?instructions?(to?calculate?offset)?on?other?architectures.
7????done?uint32
8????m????Mutex
9}
Once結構體非常簡單,其中done是調用標識符,Once對象初始化時,其done值默認為0,Once僅有一個Do()方法,當Once首次調用Do()方法后,done值變?yōu)?。m作用于初始化競態(tài)控制,在第一次調用Once.Do()方法時,會通過m加鎖,以保證在第一個Do()方法中的參數f()函數還未執(zhí)行完畢時,其他此時調用Do()方法會被阻塞(不返回也不執(zhí)行)。
Once.Do()方法的實現細節(jié)如下
1func?(o?*Once)?Do(f?func())?{
2????//?Note:?Here?is?an?incorrect?implementation?of?Do:
3????//
4????//??if?atomic.CompareAndSwapUint32(&o.done,?0,?1)?{
5????//??????f()
6????//??}
7????//
8????//?Do?guarantees?that?when?it?returns,?f?has?finished.
9????//?This?implementation?would?not?implement?that?guarantee:
10????//?given?two?simultaneous?calls,?the?winner?of?the?cas?would
11????//?call?f,?and?the?second?would?return?immediately,?without
12????//?waiting?for?the?first's?call?to?f?to?complete.
13????//?This?is?why?the?slow?path?falls?back?to?a?mutex,?and?why
14????//?the?atomic.StoreUint32?must?be?delayed?until?after?f?returns.
15
16????if?atomic.LoadUint32(&o.done)?==?0?{
17????????//?Outlined?slow-path?to?allow?inlining?of?the?fast-path.
18????????o.doSlow(f)
19????}
20}
21
22func?(o?*Once)?doSlow(f?func())?{
23????o.m.Lock()
24????defer?o.m.Unlock()
25????if?o.done?==?0?{
26????????defer?atomic.StoreUint32(&o.done,?1)
27????????f()
28????}
29}
Do()方法的入參是一個無參數輸入與返回的函數,當o.done值為0時,執(zhí)行doSlow()方法,為1則退出Do()方法。doSlow()方法很簡單:加鎖,再次檢查o.done值,執(zhí)行f(),原子操作將o.done值置為1,最后釋放鎖。
注意事項
1. 在官方示例代碼中,提到了一種錯誤實現Do()方法的方式。
1func?(o?*Once)?Do(f?func())?{
2????if?atomic.CompareAndSwapUint32(&o.done,?0,?1)?{
3????????f()
4????}
5}
當并發(fā)多次調用Do()方法時,第一個被執(zhí)行的Do()方法會將o.done值從0置為1,并執(zhí)行f(),其他的調用Do()方法會立即被返回。這種處理方式和加鎖的方式會有所不同:它不能保證在第一個調用執(zhí)行Do()方法中的f()函數被執(zhí)行完畢之前,其他的f()函數會阻塞等待。
1package?main
2
3import?(
4????"fmt"
5????"sync"
6????"time"
7)
8
9type?Config?struct?{}
10
11func?(c?*Config)?init(filename?string)?{
12????fmt.Printf("mock?[%s]?config?initial?done!\n",?filename)
13}
14
15var?(
16????once?sync.Once
17????cfg??*Config
18)
19
20func?main()?{
21????cfg?=?&Config{}
22
23????go?once.Do(func()?{
24????????time.Sleep(3?*?time.Second)
25????????cfg.init("first?file?path")
26????})
27
28????time.Sleep(time.Second)
29????once.Do(func()?{
30????????time.Sleep(time.Second)
31????????cfg.init("second?file?path")
32????})
33????fmt.Println("運行到這里!")
34????time.Sleep(5?*?time.Second)
35}
輸出
1$?go?run?main.go?
2mock?[first?file?path]?config?initial?done!
3運行到這里!
可以看到第二次調用once.Do()時候,其輸入參數f()函數雖然沒有被執(zhí)行,但是整個Do()是被阻塞的(被阻塞于o.m.Lock()處),它需要等待首次調用once.Do()執(zhí)行完畢,才會退出阻塞狀態(tài)。而錯誤實現Do()方法的方式,就無法保證此規(guī)則的實現。
2. 避免死鎖
1package?main
2
3import?(
4????"fmt"
5????"sync"
6)
7
8func?main()?{
9????once?:=?sync.Once{}
10????once.Do(func()?{
11????????fmt.Println("outside?call")
12????????once.Do(func()?{
13????????????fmt.Println("inside?call")
14????????})
15????})
16}
輸出
1$?go?run?main.go?
2outside?call
3fatal?error:?all?goroutines?are?asleep?-?deadlock!
注意,同樣由于o.m.Lock()處的代碼限定,once.Do()內部調用Do()方法時,會造成死鎖的發(fā)生。
推薦閱讀
站長 polarisxu
自己的原創(chuàng)文章
不限于 Go 技術
職場和創(chuàng)業(yè)經驗
Go語言中文網
每天為你
分享 Go 知識
Go愛好者值得關注

