目錄
- 什么是sync.Once
- 如何使用sync.Once
- 源碼分析
什么是sync.Once
Once 可以用來執(zhí)行且僅僅執(zhí)行一次動作,常常用于單例對象的初始化場景。
Once 常常用來初始化單例資源,或者并發(fā)訪問只需初始化一次的共享資源醒串,或者在測試的時候初始化一次測試資源城侧。
sync.Once 只暴露了一個方法 Do,你可以多次調(diào)用 Do 方法劫拢,但是只有第一次調(diào)用 Do 方法時 f 參數(shù)才會執(zhí)行,這里的 f 是一個無參數(shù)無返回值的函數(shù)强胰。
如何使用sync.Once
就拿我負責的一個項目來說舱沧,因為項目的配置是掛在第三方平臺上,所以在項目啟動時需要獲取資源配置偶洋,因為需要一個方法來保證配置僅此只獲取一次熟吏,因此,我們考慮使用 sync.Once 來獲取資源玄窝。這樣的話牵寺,可以防止在其他地方調(diào)用獲取資源方法,該方法僅此執(zhí)行一次恩脂。
下面我簡單寫個Demo來演示一個sync.Once如何使用
package main
import (
"fmt"
"sync"
)
var once sync.Once
var con string
func main() {
once.Do(func() {
con = "hello Test once.Do"
})
fmt.Println(con)
}
代碼說明:
代碼的話比較簡單帽氓,就是通過調(diào)用Do方法,采用閉包方式东亦,將字符串("hello Test once.Do")賦值給con杏节,進而打印出值,這就是 sync.Once 的使用典阵,比較容易上手奋渔。
但我們用一個方法或者框架時,如果不對其了如指掌壮啊,總有點不太靠譜嫉鲸,感覺心里不踏實。為此歹啼,我們來聊一聊 sync.Once 的源碼實現(xiàn)玄渗,讓他無處可遁。
源碼分析
接下來分析 sync.Do 究竟是如何實現(xiàn)的狸眼,它存儲在包sync下 once.go 文件中藤树,源代碼如下:
// sync/once.go
type Once struct {
done uint32 // 初始值為0表示還未執(zhí)行過,1表示已經(jīng)執(zhí)行過
m Mutex
}
func (o *Once) Do(f func()) {
// 判斷done是否為0拓萌,若為0岁钓,表示未執(zhí)行過,調(diào)用doSlow()方法初始化
if atomic.LoadUint32(&o.done) == 0 {
// Outlined slow-path to allow inlining of the fast-path.
o.doSlow(f)
}
}
// 加載資源
func (o *Once) doSlow(f func()) {
o.m.Lock()
defer o.m.Unlock()
// 采用雙重檢測機制 加鎖判斷done是否為零
if o.done == 0 {
// 執(zhí)行完f()函數(shù)后,將done值設置為1
defer atomic.StoreUint32(&o.done, 1)
// 執(zhí)行傳入的f()函數(shù)
f()
}
}
接下來會分為兩大部分進行分析屡限,第一部分為 Once 的結(jié)構(gòu)體組成結(jié)構(gòu)品嚣,第二部分為 Do 函數(shù)的實現(xiàn)原理,我會在代碼上加上注釋钧大,保證用心閱讀完都有收獲翰撑。
結(jié)構(gòu)體
type Once struct {
done uint32 // 初始值為0表示還未執(zhí)行過,1表示已經(jīng)執(zhí)行過
m Mutex
}
首先定義一個struct結(jié)構(gòu)體 Once 啊央,里面存儲兩個成員變量眶诈,分別為 done 和 m 。
done成員變量
- 1表示資源未初始化瓜饥,需要進一步初始化
- 0表示資源已初始化册养,無需初始化,直接返回即可
m成員變量
- 為了防止多個goroutine調(diào)用 doSlow() 初始化資源時压固,造成資源多次初始化,因此采用 Mutex 鎖機制來保證有且僅初始化一次
Do
func (o *Once) Do(f func()) {
// 判斷done是否為0靠闭,若為0帐我,表示未執(zhí)行過,調(diào)用doSlow()方法初始化
if atomic.LoadUint32(&o.done) == 0 {
// Outlined slow-path to allow inlining of the fast-path.
o.doSlow(f)
}
}
// 加載資源
func (o *Once) doSlow(f func()) {
o.m.Lock()
defer o.m.Unlock()
// 采用雙重檢測機制 加鎖判斷done是否為零
if o.done == 0 {
// 執(zhí)行完f()函數(shù)后愧膀,將done值設置為1
defer atomic.StoreUint32(&o.done, 1)
// 執(zhí)行傳入的f()函數(shù)
f()
}
調(diào)用 Do 函數(shù)時拦键,首先判斷done值是否為0,若為1檩淋,表示傳入的匿名函數(shù) f() 已執(zhí)行過芬为,無需再次執(zhí)行;若為0蟀悦,表示傳入的匿名函數(shù) f() 還未執(zhí)行過媚朦,則調(diào)用 doSlow() 函數(shù)進行初始化。
在 doSlow() 函數(shù)中日戈,若并發(fā)的goroutine進入該函數(shù)中询张,為了保證僅有一個goroutine執(zhí)行 f() 匿名函數(shù)。為此浙炼,需要加互斥鎖保證只有一個goroutine進行初始化份氧,同時采用了雙檢查的機制(double-checking),再次判斷 o.done 是否為 0弯屈,如果為 0蜗帜,則是第一次執(zhí)行,執(zhí)行完畢后资厉,就將 o.done 設置為 1厅缺,然后釋放鎖。
即使此時有多個 goroutine 同時進入了 doSlow 方法,因為雙檢查的機制店归,后續(xù)的 goroutine 會看到 o.done 的值為 1阎抒,也不會再次執(zhí)行 f。
這樣既保證了并發(fā)的 goroutine 會等待 f 完成消痛,而且還不會多次執(zhí)行 f且叁。
文章也會持續(xù)更新,可以微信搜索「 邁莫coding 」第一時間閱讀秩伞。