Golang并發(fā)利器sync.Once的用法詳解
簡介
在某些場景下,我們需要初始化一些資源,例如單例對象、配置等。實(shí)現(xiàn)資源的初始化有多種方法,如定義 package 級別的變量、在 init 函數(shù)中進(jìn)行初始化,或者在 main 函數(shù)中進(jìn)行初始化。這三種方式都能確保并發(fā)安全,并在程序啟動時(shí)完成資源的初始化。
然而,有時(shí)我們希望采用延遲初始化的方式,在我們真正需要資源的時(shí)候才進(jìn)行初始化,這種需要確保并發(fā)安全,在這種情況下,Go 語言中的 sync.Once 提供一個(gè)優(yōu)雅且并發(fā)安全的解決方案,本文將對其進(jìn)行介紹。
sync.Once 基本概念
什么是 sync.Once
sync.Once 是 Go 語言中的一種同步原語,用于確保某個(gè)操作或函數(shù)在并發(fā)環(huán)境下只被執(zhí)行一次。它只有一個(gè)導(dǎo)出的方法,即 Do,該方法接收一個(gè)函數(shù)參數(shù)。在 Do 方法被調(diào)用后,該函數(shù)將被執(zhí)行,而且只會執(zhí)行一次,即使在多個(gè)協(xié)程同時(shí)調(diào)用的情況下也是如此。
sync.Once 的應(yīng)用場景
sync.Once 主要用于以下場景:
- 單例模式:確保全局只有一個(gè)實(shí)例對象,避免重復(fù)創(chuàng)建資源。
- 延遲初始化:在程序運(yùn)行過程中需要用到某個(gè)資源時(shí),通過
sync.Once動態(tài)地初始化該資源。 - 只執(zhí)行一次的操作:例如只需要執(zhí)行一次的配置加載、數(shù)據(jù)清理等操作。
sync.Once 應(yīng)用實(shí)例
單例模式
在單例模式中,我們需要確保一個(gè)結(jié)構(gòu)體只被初始化一次。使用 sync.Once 可以輕松實(shí)現(xiàn)這一目標(biāo)。
package main
import (
"fmt"
"sync"
)
type Singleton struct{}
var (
instance *Singleton
once sync.Once
)
func GetInstance() *Singleton {
once.Do(func() {
instance = &Singleton{}
})
return instance
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
s := GetInstance()
fmt.Printf("Singleton instance address: %p\n", s)
}()
}
wg.Wait()
}上述代碼中,GetInstance 函數(shù)通過 once.Do() 確保 instance 只會被初始化一次。在并發(fā)環(huán)境下,多個(gè)協(xié)程同時(shí)調(diào)用 GetInstance 時(shí),只有一個(gè)協(xié)程會執(zhí)行 instance = &Singleton{},所有協(xié)程得到的實(shí)例 s 都是同一個(gè)。
延遲初始化
有時(shí)候希望在需要時(shí)才初始化某些資源。使用 sync.Once 可以實(shí)現(xiàn)這一目標(biāo)。
package main
import (
"fmt"
"sync"
)
type Config struct {
config map[string]string
}
var (
config *Config
once sync.Once
)
func GetConfig() *Config {
once.Do(func() {
fmt.Println("init config...")
config = &Config{
config: map[string]string{
"c1": "v1",
"c2": "v2",
},
}
})
return config
}
func main() {
// 第一次需要獲取配置信息,初始化 config
cfg := GetConfig()
fmt.Println("c1: ", cfg.config["c1"])
// 第二次需要,此時(shí) config 已經(jīng)被初始化過,無需再次初始化
cfg2 := GetConfig()
fmt.Println("c2: ", cfg2.config["c2"])
}在這個(gè)示例中,定義了一個(gè) Config 結(jié)構(gòu)體,它包含一些設(shè)置信息。使用 sync.Once 來實(shí)現(xiàn) GetConfig 函數(shù),該函數(shù)在第一次調(diào)用時(shí)初始化 Config。這樣,我們可以在真正需要時(shí)才初始化 Config,從而避免不必要的開銷。
sync.Once 實(shí)現(xiàn)原理
type Once struct {
// 表示是否執(zhí)行了操作
done uint32
// 互斥鎖,確保多個(gè)協(xié)程訪問時(shí),只能一個(gè)協(xié)程執(zhí)行操作
m Mutex
}
func (o *Once) Do(f func()) {
// 判斷 done 的值,如果是 0,說明 f 還沒有被執(zhí)行過
if atomic.LoadUint32(&o.done) == 0 {
// 構(gòu)建慢路徑(slow-path),以允許對 Do 方法的快路徑(fast-path)進(jìn)行內(nèi)聯(lián)
o.doSlow(f)
}
}
func (o *Once) doSlow(f func()) {
// 加鎖
o.m.Lock()
defer o.m.Unlock()
// 雙重檢查,避免 f 已被執(zhí)行過
if o.done == 0 {
// 修改 done 的值
defer atomic.StoreUint32(&o.done, 1)
// 執(zhí)行函數(shù)
f()
}
}sync.Once 結(jié)構(gòu)體包含兩個(gè)字段:done 和 mu。done 是一個(gè) uint32 類型的變量,用于表示操作是否已經(jīng)執(zhí)行過;m 是一個(gè)互斥鎖,用于確保在多個(gè)協(xié)程訪問時(shí),只有一個(gè)協(xié)程能執(zhí)行操作。
sync.Once 結(jié)構(gòu)體包含兩個(gè)方法:Do 和 doSlow。Do 方法是其核心方法,它接收一個(gè)函數(shù)參數(shù) f。首先它會通過原子操作atomic.LoadUint32(保證并發(fā)安全) 檢查 done 的值,如果為 0,表示 f 函數(shù)沒有被執(zhí)行過,然后執(zhí)行 doSlow 方法。
在 doSlow 方法里,首先對互斥鎖 m 進(jìn)行加鎖,確保在多個(gè)協(xié)程訪問時(shí),只有一個(gè)協(xié)程能執(zhí)行 f 函數(shù)。接著再次檢查 done 變量的值,如果 done 的值仍為 0,說明 f 函數(shù)沒有被執(zhí)行過,此時(shí)執(zhí)行 f 函數(shù),最后通過原子操作 atomic.StoreUint32 將 done 變量的值設(shè)置為 1。
為什么會封裝一個(gè) doSlow 方法
doSlow 方法的存在主要是為了性能優(yōu)化。將慢路徑(slow-path)代碼從 Do 方法中分離出來,使得 Do 方法的快路徑(fast-path)能夠被內(nèi)聯(lián)(inlined),從而提高性能。
為什么會有雙重檢查(double check)的寫法
從源碼可知,存在兩次對 done 的值的判斷。
- 第一次檢查:在獲取鎖之前,先使用原子加載操作
atomic.LoadUint32檢查done變量的值,如果done的值為 1,表示操作已執(zhí)行,此時(shí)直接返回,不再執(zhí)行doSlow方法。這一檢查可以避免不必要的鎖競爭。 - 第二次檢查:獲取鎖之后,再次檢查
done變量的值,這一檢查是為了確保在當(dāng)前協(xié)程獲取鎖期間,其他協(xié)程沒有執(zhí)行過f函數(shù)。如果done的值仍為 0,表示f函數(shù)沒有被執(zhí)行過。
通過雙重檢查,可以在大多數(shù)情況下避免鎖競爭,提高性能。
加強(qiáng)的 sync.Once
sync.Once 提供的 Do 方法并沒有返回值,意味著如果我們傳入的函數(shù)如果發(fā)生 error 導(dǎo)致初始化失敗,后續(xù)調(diào)用 Do 方法也不會再初始化。為了避免這個(gè)問題,我們可以實(shí)現(xiàn)一個(gè) 類似 sync.Once 的并發(fā)原語。
package main
import (
"sync"
"sync/atomic"
)
type Once struct {
done uint32
m sync.Mutex
}
func (o *Once) Do(f func() error) error {
if atomic.LoadUint32(&o.done) == 0 {
return o.doSlow(f)
}
return nil
}
func (o *Once) doSlow(f func() error) error {
o.m.Lock()
defer o.m.Unlock()
var err error
if o.done == 0 {
err = f()
// 只有沒有 error 的時(shí)候,才修改 done 的值
if err == nil {
atomic.StoreUint32(&o.done, 1)
}
}
return err
}上述代碼實(shí)現(xiàn)了一個(gè)加強(qiáng)的 Once 結(jié)構(gòu)體。與標(biāo)準(zhǔn)的 sync.Once 不同,這個(gè)實(shí)現(xiàn)允許 Do 方法的函數(shù)參數(shù)返回一個(gè) error。如果執(zhí)行函數(shù)沒有返回 error,則修改 done 的值以表示函數(shù)已執(zhí)行。這樣,在后續(xù)的調(diào)用中,只有在沒有發(fā)生 error 的情況下,才會跳過函數(shù)執(zhí)行,避免初始化失敗。
sync.Once 的注意事項(xiàng)
死鎖
通過分析 sync.Once 的源碼,可以看到它包含一個(gè)名為 m 的互斥鎖字段。當(dāng)我們在 Do 方法內(nèi)部重復(fù)調(diào)用 Do 方法時(shí),將會多次嘗試獲取相同的鎖。但是 mutex 互斥鎖并不支持可重入操作,因此這將導(dǎo)致死鎖現(xiàn)象。
func main() {
once := sync.Once{}
once.Do(func() {
once.Do(func() {
fmt.Println("init...")
})
})
}初始化失敗
這里的初始化失敗指的是在調(diào)用 Do 方法之后,執(zhí)行 f 函數(shù)的過程中發(fā)生 error,導(dǎo)致執(zhí)行失敗,現(xiàn)有的 sync.Once 設(shè)計(jì)我們是無法感知到初始化的失敗的,為了解決這個(gè)問題,我們可以實(shí)現(xiàn)一個(gè)類似 sync.Once 的加強(qiáng) once,前面的內(nèi)容已經(jīng)提供了具體實(shí)現(xiàn)。
小結(jié)
本文詳細(xì)介紹了 Go 語言中的 sync.Once,包括它的基本定義、使用場景和應(yīng)用實(shí)例以及源碼分析等。在實(shí)際開發(fā)中,sync.Once 經(jīng)常被用于實(shí)現(xiàn)單例模式和延遲初始化操作。
雖然 sync.Once 簡單而又高效,但是錯誤的使用可能會造成一些意外情況,需要格外小心。
總之,sync.Once 是 Go 中非常實(shí)用的一個(gè)并發(fā)原語,可以幫助開發(fā)者實(shí)現(xiàn)各種并發(fā)場景下的安全操作。如果遇到只需要初始化一次的場景,sync.Once 是一個(gè)非常好的選擇。
以上就是Golang并發(fā)利器sync.Once的用法詳解的詳細(xì)內(nèi)容,更多關(guān)于Golang sync.Once的資料請關(guān)注腳本之家其它相關(guān)文章!
- Go語言標(biāo)準(zhǔn)庫sync.Once使用場景及性能優(yōu)化詳解
- golang使用sync.Once實(shí)現(xiàn)懶加載的用法和坑點(diǎn)詳解
- golang中sync.Once只執(zhí)行一次的原理解析
- go并發(fā)利器sync.Once使用示例詳解
- go?sync.Once實(shí)現(xiàn)高效單例模式詳解
- Golang基于sync.Once實(shí)現(xiàn)單例的操作代碼
- 一文解析 Golang sync.Once 用法及原理
- Go并發(fā)編程之sync.Once使用實(shí)例詳解
- Go語言并發(fā)編程 sync.Once
- 深入理解go sync.Once的具體使用
相關(guān)文章
詳解Go語言如何熱重載和優(yōu)雅地關(guān)閉程序
我們有時(shí)會因不同的目的去關(guān)閉服務(wù),一種關(guān)閉服務(wù)是終止操作系統(tǒng),一種關(guān)閉服務(wù)是用來更新配置,本文就來和大家簡單講講這兩種方法的實(shí)現(xiàn)吧2023-07-07
go語言goto語句跳轉(zhuǎn)到指定的標(biāo)簽實(shí)現(xiàn)方法
這篇文章主要介紹了go語言goto語句跳轉(zhuǎn)到指定的標(biāo)簽實(shí)現(xiàn)方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-05-05
使用Go語言實(shí)現(xiàn)跨域資源共享(CORS)設(shè)置
在Web開發(fā)中,跨域資源共享(CORS)是一種重要的安全機(jī)制,它允許許多資源在一個(gè)網(wǎng)頁上被另一個(gè)來源的網(wǎng)頁所訪問,然而,出于安全考慮,瀏覽器默認(rèn)禁止這種跨域訪問,為了解決這個(gè)問題,我們可以使用Go語言來設(shè)置CORS,需要的朋友可以參考下2024-06-06
Go語言同步等待組sync.WaitGroup結(jié)構(gòu)體對象方法詳解
這篇文章主要為大家介紹了Go語言同步等待組sync.WaitGroup結(jié)構(gòu)體對象方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08
Go語言使用MongoDB數(shù)據(jù)庫詳細(xì)步驟
mongodb是一種高性能、開源、文檔型的nosql數(shù)據(jù)庫,被廣泛應(yīng)用于web應(yīng)用、大數(shù)據(jù)以及云計(jì)算領(lǐng)域,下面這篇文章主要給大家介紹了關(guān)于Go語言使用MongoDB數(shù)據(jù)庫的詳細(xì)步驟,需要的朋友可以參考下2024-05-05

