go并發(fā)利器sync.Once使用示例詳解
1. 簡(jiǎn)介
本文主要介紹 Go 語(yǔ)言中的 Once 并發(fā)原語(yǔ),包括 Once 的基本使用方法、原理和注意事項(xiàng),從而對(duì) Once 的使用有基本的了解。
2. 基本使用
2.1 基本定義
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ù)。
2.2 使用方式
使用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ù)代碼
}
2.3 使用例子
下面是一個(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變量。
3. 原理
下面是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ù)還沒(méi)有被執(zhí)行過(guò),此時(shí)會(huì)加鎖并執(zhí)行傳入的函數(shù),并將done標(biāo)志位置為1,然后釋放鎖。如果標(biāo)志位為1,說(shuō)明函數(shù)已經(jīng)被執(zhí)行過(guò)了,直接返回。
4. 使用注意事項(xiàng)
4.1 不能將sync.Once作為函數(shù)局部變量
下面是一個(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)使用。
4.2 不能在once.Do中再次調(diào)用once.Do
下面舉一個(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方法。
4.3 需要對(duì)傳入的函數(shù)進(jìn)行錯(cuò)誤處理
4.3.1 基本說(shuō)明
一般情況下,如果傳入的函數(shù)不會(huì)出現(xiàn)錯(cuò)誤,可以不進(jìn)行錯(cuò)誤處理。但是,如果傳入的函數(shù)可能出現(xiàn)錯(cuò)誤,就必須對(duì)其進(jìn)行錯(cuò)誤處理,否則可能會(huì)導(dǎo)致程序崩潰或出現(xiàn)不可預(yù)料的錯(cuò)誤。
因此,在編寫(xiě)傳入Once的Do方法的函數(shù)時(shí),需要考慮到錯(cuò)誤處理問(wèn)題,保證程序的健壯性和穩(wěn)定性。
4.3.2 未錯(cuò)誤處理導(dǎo)致的問(wèn)題
下面舉一個(gè)傳入的函數(shù)可能出現(xiàn)錯(cuò)誤,但是沒(méi)有對(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ò)誤的,而這里并沒(méi)有進(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 處理方式
- 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)用。因此,在編寫(xiě)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í)行初始化邏輯。
5. 總結(jié)
本文旨在介紹Go語(yǔ)言中的Once并發(fā)原語(yǔ),包括其基本使用、原理和注意事項(xiàng),讓大家對(duì)Once有一個(gè)基本的了解。
首先,我們通過(guò)示例演示了Once的基本使用方法,并強(qiáng)調(diào)了其僅會(huì)執(zhí)行一次的特性。然后,我們解釋了Once僅執(zhí)行一次的原因,使讀者更好地理解Once的工作原理。最后,我們指出了使用Once時(shí)的一些注意事項(xiàng),以避免誤用。
總之,本文全面地介紹了Go語(yǔ)言中的Once并發(fā)原語(yǔ),使讀者能夠更好地理解和應(yīng)用它。
以上就是go并發(fā)利器sync.Once使用示例詳解的詳細(xì)內(nèi)容,更多關(guān)于go并發(fā)利器sync.Once的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- Go語(yǔ)言標(biāo)準(zhǔn)庫(kù)sync.Once使用場(chǎng)景及性能優(yōu)化詳解
- golang使用sync.Once實(shí)現(xiàn)懶加載的用法和坑點(diǎn)詳解
- golang中sync.Once只執(zhí)行一次的原理解析
- Golang并發(fā)利器sync.Once的用法詳解
- go?sync.Once實(shí)現(xiàn)高效單例模式詳解
- Golang基于sync.Once實(shí)現(xiàn)單例的操作代碼
- 一文解析 Golang sync.Once 用法及原理
- Go并發(fā)編程之sync.Once使用實(shí)例詳解
- Go語(yǔ)言并發(fā)編程 sync.Once
- 深入理解go sync.Once的具體使用
相關(guān)文章
golang獲取變量或?qū)ο箢愋偷膸追N方式總結(jié)
在golang中并沒(méi)有提供內(nèi)置函數(shù)來(lái)獲取變量的類型,但是通過(guò)一定的方式也可以獲取,下面這篇文章主要給大家介紹了關(guān)于golang獲取變量或?qū)ο箢愋偷膸追N方式,需要的朋友可以參考下2022-12-12
詳解Golang互斥鎖內(nèi)部實(shí)現(xiàn)
本篇文章主要介紹了詳解Golang互斥鎖內(nèi)部實(shí)現(xiàn),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-06-06
Go語(yǔ)言實(shí)現(xiàn)順序存儲(chǔ)的線性表實(shí)例
這篇文章主要介紹了Go語(yǔ)言實(shí)現(xiàn)順序存儲(chǔ)的線性表的方法,實(shí)例分析了Go語(yǔ)言實(shí)現(xiàn)線性表的定義、插入、刪除元素等的使用技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-03-03
go內(nèi)存緩存如何new一個(gè)bigcache對(duì)象示例詳解
這篇文章主要為大家介紹了go內(nèi)存緩存如何new一個(gè)bigcache對(duì)象示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-09-09
GO利用channel協(xié)調(diào)協(xié)程的實(shí)現(xiàn)
本文主要介紹了GO利用channel協(xié)調(diào)協(xié)程的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-05-05
Golang使用bcrypt實(shí)現(xiàn)密碼加密和校驗(yàn)的操作代碼
bcrypt可以用于數(shù)據(jù)庫(kù)中的用戶密碼保存,相比md5而言更加的安全可靠,這篇文章主要介紹了Golang使用bcrypt實(shí)現(xiàn)密碼加密和校驗(yàn)的操作代碼,需要的朋友可以參考下2024-05-05
詳解Go語(yǔ)言中Validator庫(kù)的使用方法和用途
github.com/go-playground/validator 是一個(gè) Go 語(yǔ)言的庫(kù),用于對(duì)結(jié)構(gòu)體字段進(jìn)行驗(yàn)證,它提供了一種簡(jiǎn)單而靈活的方式來(lái)定義驗(yàn)證規(guī)則,在這篇文章中,我們將從一個(gè)簡(jiǎn)單的問(wèn)題出發(fā),帶你了解 Validator 庫(kù)的用途,也會(huì)介紹Validator 的基本使用2023-09-09
深入解析Go語(yǔ)言的io.ioutil標(biāo)準(zhǔn)庫(kù)使用
這篇文章主要介紹了Go語(yǔ)言的io.ioutil標(biāo)準(zhǔn)庫(kù)使用,是Golang入門(mén)學(xué)習(xí)中的基礎(chǔ)知識(shí),需要的朋友可以參考下2015-10-10
golang 40行代碼實(shí)現(xiàn)通用協(xié)程池
golang協(xié)程機(jī)制很方便的解決了并發(fā)編程的問(wèn)題,但是協(xié)程并不是沒(méi)有開(kāi)銷的,所以也需要適當(dāng)限制一下數(shù)量。這篇文章主要介紹了golang 40行代碼實(shí)現(xiàn)通用協(xié)程池,需要的朋友可以參考下2018-08-08

