您好,登錄后才能下訂單哦!
這篇文章主要介紹了go并發(fā)利器sync.Once如何使用的相關(guān)知識(shí),內(nèi)容詳細(xì)易懂,操作簡(jiǎn)單快捷,具有一定借鑒價(jià)值,相信大家閱讀完這篇go并發(fā)利器sync.Once如何使用文章都會(huì)有所收獲,下面我們一起來(lái)看看吧。
本文主要介紹 Go 語(yǔ)言中的 Once 并發(fā)原語(yǔ),包括 Once 的基本使用方法、原理和注意事項(xiàng),從而對(duì) Once 的使用有基本的了解。
sync.Once
是Go語(yǔ)言中的一個(gè)并發(fā)原語(yǔ),用于保證某個(gè)函數(shù)只被執(zhí)行一次。Once
類型有一個(gè)Do
方法,該方法接收一個(gè)函數(shù)作為參數(shù),并在第一次調(diào)用時(shí)執(zhí)行該函數(shù)。如果Do
方法被多次調(diào)用,只有第一次調(diào)用會(huì)執(zhí)行傳入的函數(shù)。
使用sync.Once
非常簡(jiǎn)單,只需要?jiǎng)?chuàng)建一個(gè)Once
類型的變量,然后在需要保證函數(shù)只被執(zhí)行一次的地方調(diào)用其Do
方法即可。下面是一個(gè)簡(jiǎn)單的例子:
var once sync.Once func initOperation() { // 這里執(zhí)行一些初始化操作,只會(huì)被執(zhí)行一次 } func main() { // 在程序啟動(dòng)時(shí)執(zhí)行initOperation函數(shù),保證初始化只被執(zhí)行一次 once.Do(initOperation) // 后續(xù)代碼 }
下面是一個(gè)簡(jiǎn)單使用sync.Once
的例子,其中我們使用sync.Once
來(lái)保證全局變量config只會(huì)被初始化一次:
package main import ( "fmt" "sync" ) var ( config map[string]string once sync.Once ) func loadConfig() { // 模擬從配置文件中加載配置信息 fmt.Println("load config...") config = make(map[string]string) config["host"] = "127.0.0.1" config["port"] = "8080" } func GetConfig() map[string]string { once.Do(loadConfig) return config } func main() { // 第一次調(diào)用GetConfig會(huì)執(zhí)行l(wèi)oadConfig函數(shù),初始化config變量 fmt.Println(GetConfig()) // 第二次調(diào)用GetConfig不會(huì)執(zhí)行l(wèi)oadConfig函數(shù),直接返回已初始化的config變量 fmt.Println(GetConfig()) }
在這個(gè)例子中,我們定義了一個(gè)全局變量config
和一個(gè)sync.Once
類型的變量once
。在GetConfig
函數(shù)中,我們通過(guò)調(diào)用once.Do
方法來(lái)保證loadConfig
函數(shù)只會(huì)被執(zhí)行一次,從而保證config
變量只會(huì)被初始化一次。 運(yùn)行上面的程序,輸出如下:
load config... map[host:127.0.0.1 port:8080] map[host:127.0.0.1 port:8080]
可以看到,GetConfig
函數(shù)在第一次調(diào)用時(shí)執(zhí)行了loadConfig
函數(shù),初始化了config
變量。在第二次調(diào)用時(shí),loadConfig
函數(shù)不會(huì)被執(zhí)行,直接返回已經(jīng)初始化的config
變量。
下面是sync.Once
的具體實(shí)現(xiàn)如下:
type Once struct { done uint32 m Mutex } func (o *Once) Do(f func()) { // 判斷done標(biāo)記位是否為0 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() // 執(zhí)行雙重檢查,再次判斷函數(shù)是否已經(jīng)執(zhí)行 if o.done == 0 { defer atomic.StoreUint32(&o.done, 1) f() } }
sync.Once
的實(shí)現(xiàn)原理比較簡(jiǎn)單,主要依賴于一個(gè)done
標(biāo)志位和一個(gè)互斥鎖。當(dāng)Do
方法被第一次調(diào)用時(shí),會(huì)先原子地讀取done
標(biāo)志位,如果該標(biāo)志位為0,說(shuō)明函數(shù)還沒有被執(zhí)行過(guò),此時(shí)會(huì)加鎖并執(zhí)行傳入的函數(shù),并將done
標(biāo)志位置為1,然后釋放鎖。如果標(biāo)志位為1,說(shuō)明函數(shù)已經(jīng)被執(zhí)行過(guò)了,直接返回。
下面是一個(gè)簡(jiǎn)單的例子,說(shuō)明將 sync.Once
作為局部變量會(huì)導(dǎo)致的問(wèn)題:
var config map[string]string func initConfig() { fmt.Println("initConfig called") config["1"] = "hello world" } func getConfig() map[string]string{ var once sync.Once once.Do(initCount) fmt.Println("getConfig called") } func main() { for i := 0; i < 10; i++ { go getConfig() } time.Sleep(time.Second) }
這里初始化函數(shù)會(huì)被多次調(diào)用,這與initConfig
方法只會(huì)執(zhí)行一次的預(yù)期不符。這是因?yàn)閷?sync.Once
作為局部變量時(shí),每次調(diào)用函數(shù)都會(huì)創(chuàng)建新的 sync.Once
實(shí)例,每個(gè) sync.Once
實(shí)例都有自己的 done
標(biāo)志,多個(gè)實(shí)例之間無(wú)法共享狀態(tài)。導(dǎo)致初始化函數(shù)會(huì)被多次調(diào)用。
如果將 sync.Once
作為全局變量或包級(jí)別變量,就可以避免這個(gè)問(wèn)題。所以基于此,不能定義sync.Once
作為函數(shù)局部變量來(lái)使用。
下面舉一個(gè)在once.Do
方法中再次調(diào)用once.Do
方法的例子:
package main import ( "fmt" "sync" ) func main() { var once sync.Once var onceBody func() onceBody = func() { fmt.Println("Only once") once.Do(onceBody) // 再次調(diào)用once.Do方法 } // 執(zhí)行once.Do方法 once.Do(onceBody) fmt.Println("done") }
在上述代碼中,當(dāng)once.Do(onceBody)
第一次執(zhí)行時(shí),會(huì)輸出"Only once",然后在執(zhí)行once.Do(onceBody)
時(shí)會(huì)發(fā)生死鎖,程序無(wú)法繼續(xù)執(zhí)行下去。
這是因?yàn)?code>once.Do()方法在執(zhí)行過(guò)程中會(huì)獲取互斥鎖,在方法內(nèi)再次調(diào)用once.Do()
方法,那么就會(huì)在獲取互斥鎖時(shí)出現(xiàn)死鎖。
因此,我們不能在once.Do方法中再次調(diào)用once.Do方法。
一般情況下,如果傳入的函數(shù)不會(huì)出現(xiàn)錯(cuò)誤,可以不進(jìn)行錯(cuò)誤處理。但是,如果傳入的函數(shù)可能出現(xiàn)錯(cuò)誤,就必須對(duì)其進(jìn)行錯(cuò)誤處理,否則可能會(huì)導(dǎo)致程序崩潰或出現(xiàn)不可預(yù)料的錯(cuò)誤。
因此,在編寫傳入Once的Do方法的函數(shù)時(shí),需要考慮到錯(cuò)誤處理問(wèn)題,保證程序的健壯性和穩(wěn)定性。
下面舉一個(gè)傳入的函數(shù)可能出現(xiàn)錯(cuò)誤,但是沒有對(duì)其進(jìn)行錯(cuò)誤處理的例子:
import ( "fmt" "net" "sync" ) var ( initialized bool connection net.Conn initOnce sync.Once ) func initConnection() { connection, _ = net.Dial("tcp", "err_address") } func getConnection() net.Conn { initOnce.Do(initConnection) return connection } func main() { conn := getConnection() fmt.Println(conn) conn.Close() }
在上面例子中,其中initConnection
為傳入的函數(shù),用于建立TCP網(wǎng)絡(luò)連接,但是在sync.Once
中執(zhí)行該函數(shù)時(shí),是有可能返回錯(cuò)誤的,而這里并沒有進(jìn)行錯(cuò)誤處理,直接忽略掉錯(cuò)誤。此時(shí)調(diào)用getConnection
方法,如果initConnection
報(bào)錯(cuò)的話,獲取連接時(shí)會(huì)返回空連接,后續(xù)調(diào)用將會(huì)出現(xiàn)空指針異常。因此,如果傳入sync.Once
當(dāng)中的函數(shù)可能發(fā)生異常,此時(shí)應(yīng)該需要對(duì)其進(jìn)行處理。
4.3.3.1 panic退出執(zhí)行
應(yīng)用程序第一次啟動(dòng)時(shí),此時(shí)調(diào)用sync.Once
來(lái)初始化一些資源,此時(shí)發(fā)生錯(cuò)誤,同時(shí)初始化的資源是必須初始化的,可以考慮在出現(xiàn)錯(cuò)誤的情況下,使用panic將程序退出,避免程序繼續(xù)執(zhí)行導(dǎo)致更大的問(wèn)題。具體代碼示例如下:
import ( "fmt" "net" "sync" ) var ( connection net.Conn initOnce sync.Once ) func initConnection() { // 嘗試建立連接 connection, err = net.Dial("tcp", "err_address") if err != nil { panic("net.Dial error") } } func getConnection() net.Conn { initOnce.Do(initConnection) return connection }
如上,當(dāng)initConnection方法報(bào)錯(cuò)后,此時(shí)我們直接panic,退出整個(gè)程序的執(zhí)行。
4.3.3.2 修改sync.Once
實(shí)現(xiàn),Do函數(shù)的語(yǔ)意修改為只成功執(zhí)行一次
在程序運(yùn)行過(guò)程中,可以選擇記錄下日志或者返回錯(cuò)誤碼,而不需要中斷程序的執(zhí)行。然后下次調(diào)用時(shí)再執(zhí)行初始化的邏輯。這里需要對(duì)sync.Once
進(jìn)行改造,原本sync.Once
中Do函數(shù)的實(shí)現(xiàn)為執(zhí)行一次,這里將其修改為只成功執(zhí)行一次。具體使用方式需要根據(jù)具體業(yè)務(wù)場(chǎng)景來(lái)決定。下面是其中一個(gè)實(shí)現(xiàn):
type MyOnce struct { done int32 m sync.Mutex } func (o *MyOnce) Do(f func() error) { if atomic.LoadInt32(&o.done) == 0 { o.doSlow(f) } } func (o *MyOnce) doSlow(f func() error) { o.m.Lock() defer o.m.Unlock() if o.done == 0 { // 只有在函數(shù)調(diào)用不返回err時(shí),才會(huì)設(shè)置done if err := f(); err == nil { atomic.StoreInt32(&o.done, 1) } } }
上述代碼中,增加了一個(gè)錯(cuò)誤處理邏輯。當(dāng) f()
函數(shù)返回錯(cuò)誤時(shí),不會(huì)將 done
標(biāo)記位置為 1,以便下次調(diào)用時(shí)可以重新執(zhí)行初始化邏輯。
需要注意的是,這種方式雖然可以解決初始化失敗后的問(wèn)題,但可能會(huì)導(dǎo)致初始化函數(shù)被多次調(diào)用。因此,在編寫f()
函數(shù)時(shí),需要考慮到這個(gè)問(wèn)題,以避免出現(xiàn)不可預(yù)期的結(jié)果。
下面是一個(gè)簡(jiǎn)單的例子,使用我們重新實(shí)現(xiàn)的Once,展示第一次初始化失敗時(shí),第二次調(diào)用會(huì)重新執(zhí)行初始化邏輯,并成功初始化:
var ( hasCall bool conn net.Conn m MyOnce ) func initConn() (net.Conn, error) { fmt.Println("initConn...") // 第一次執(zhí)行,直接返回錯(cuò)誤 if !hasCall { return nil, errors.New("init error") } // 第二次執(zhí)行,初始化成功,這里默認(rèn)其成功 conn, _ = net.Dial("tcp", "baidu.com:80") return conn, nil } func GetConn() (net.Conn, error) { m.Do(func() error { var err error conn, err = initConn() if err != nil { return err } return nil }) // 第一次執(zhí)行之后,將hasCall設(shè)置為true,讓其執(zhí)行初始化邏輯 hasCall = true return conn, nil } func main() { // 第一次執(zhí)行初始化邏輯,失敗 GetConn() // 第二次執(zhí)行初始化邏輯,還是會(huì)執(zhí)行,此次執(zhí)行成功 GetConn() // 第二次執(zhí)行成功,第三次調(diào)用,將不會(huì)執(zhí)行初始化邏輯 GetConn() }
在這個(gè)例子中,第一次調(diào)用Do
方法初始化失敗了,done
標(biāo)記位被設(shè)置為0。在第二次調(diào)用Do
方法時(shí),由于done
標(biāo)記位為0,會(huì)重新執(zhí)行初始化邏輯,這次初始化成功了,done
標(biāo)記位被設(shè)置為1。第三次調(diào)用,由于之前Do
方法已經(jīng)執(zhí)行成功了,不會(huì)再執(zhí)行初始化邏輯。
關(guān)于“go并發(fā)利器sync.Once如何使用”這篇文章的內(nèi)容就介紹到這里,感謝各位的閱讀!相信大家對(duì)“go并發(fā)利器sync.Once如何使用”知識(shí)都有一定的了解,大家如果還想學(xué)習(xí)更多知識(shí),歡迎關(guān)注億速云行業(yè)資訊頻道。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。