Go中sync.Once源碼的深度講解
概念
sync.Once
是Go語言標(biāo)準(zhǔn)庫中的一個(gè)同步原語,用于確保某個(gè)操作只執(zhí)行一次。它在多線程環(huán)境中非常有用,尤其是在需要初始化共享資源或執(zhí)行某些一次性任務(wù)時(shí)。
簡單示例
當(dāng)我們在web服務(wù)訪問某個(gè)路由時(shí),如果需要事先獲取某些配置,往往會寫一個(gè)loadConfig
函數(shù),獲取一個(gè)cfg
配置項(xiàng)。多次路由訪問所需要獲取的配置項(xiàng)通常是相同的,如果對于每次路由訪問,都加載一次loadConfig
函數(shù),會導(dǎo)致產(chǎn)生一些不必要的開銷。如果loadConfig
涉及到讀取文件、解析配置、網(wǎng)絡(luò)請求時(shí),有可能會額外增加的請求響應(yīng)時(shí)間,降低服務(wù)的吞吐量。使用sync.Once
包提供的Do
函數(shù),就可以只在第一次請求時(shí)調(diào)用loadConfig
函數(shù)加載配置,之后的請求都復(fù)用第一次請求的配置,縮短響應(yīng)時(shí)間。
package main import ( "log" "net/http" "sync" ) type Config struct { APIKey string LogLevel string } var ( config *Config once sync.Once ) func loadConfig() { // 模擬從文件或環(huán)境變量加載配置 config = &Config{ APIKey: "secret-key", LogLevel: "debug", } log.Println("Configuration loaded") } func GetConfig() *Config { once.Do(loadConfig) // 僅第一次訪問時(shí)會執(zhí)行l(wèi)oadConfig函數(shù) return config } func handler(w http.ResponseWriter, r *http.Request) { cfg := GetConfig() log.Printf("Request handled with API key: %s", cfg.APIKey) w.Write([]byte("OK")) } func main() { http.HandleFunc("/", handler) log.Fatal(http.ListenAndServe(":8888", nil)) }
源碼解讀
源碼文件:src/sync/once.go (go 1.23 版本)
package sync import ( "sync/atomic" ) type Once struct { done atomic.Uint32 // 是否已執(zhí)行標(biāo)識位,0-未執(zhí)行 1-已執(zhí)行 m Mutex // 互斥鎖,確保并發(fā)安全 } func (o *Once) Do(f func()) { // 第一次執(zhí)行Do函數(shù)時(shí),原子操作檢查o.done==0,執(zhí)行doSlow函數(shù)后,o.done==1 // 第二次及之后執(zhí)行Do函數(shù),原子操作檢查o.done標(biāo)識位為1,Do函數(shù)不執(zhí)行任何功能,確保了f函數(shù)只在第一次被執(zhí)行 if o.done.Load() == 0 { o.doSlow(f) // 調(diào)用doSlow函數(shù)執(zhí)行f方法。第一次執(zhí)行時(shí),同一時(shí)間可能有多個(gè)goroutine嘗試同時(shí)執(zhí)行doSlow函數(shù) } } func (o *Once) doSlow(f func()) { o.m.Lock() // 加鎖保護(hù),避免多個(gè)goroutine同時(shí)繞過之前的原子操作檢查,并發(fā)修改o.done的值 defer o.m.Unlock() // 二次檢查o.done的值,同一時(shí)間并發(fā)執(zhí)行doSlow函數(shù)的goroutine,在第一個(gè)goroutine將o.done置為1并解除互斥鎖后, // 剩下的goroutine識別到自身的o.done已經(jīng)被設(shè)為1,無法繞過二次檢查 if o.done.Load() == 0 { defer o.done.Store(1) // 需要在f()函數(shù)執(zhí)行完成之后,原子性地將o.done設(shè)為1 f() // 執(zhí)行f方法,一定只有一個(gè)goroutine會調(diào)用這個(gè)方法 } }
可以看到,once.go
文件的代碼非常精煉。僅定義了一個(gè)含2個(gè)非導(dǎo)出字段done
和m
的結(jié)構(gòu)體Once
,并提供了一個(gè)doSlow
方法用于執(zhí)行f
函數(shù)。當(dāng)我們調(diào)用Do
方法時(shí),程序經(jīng)歷了幾個(gè)關(guān)鍵步驟:
- 判斷
done
標(biāo)志位是否等于0,如果是,說明f
函數(shù)還沒有被執(zhí)行,執(zhí)行doSlow
方法 mu
互斥鎖加鎖,防止多個(gè)goroutine
并發(fā)操作- double-check
done
標(biāo)志位是否等于0,如果是,說明f
函數(shù)還沒有被執(zhí)行,執(zhí)行f
函數(shù) f
函數(shù)執(zhí)行完成之后,再將done
標(biāo)志位原子性設(shè)為1。使用原子操作是從內(nèi)存可見性的角度出發(fā),如果done
使用uint32
而不是atomic.Uint32
,done
修改可能不會立即被其它goroutine
感知,解鎖后仍有可能存在goroutine
的done
等于0,重復(fù)執(zhí)行f
函數(shù)mu
互斥鎖解鎖。此時(shí)進(jìn)入到doSlow
函數(shù)的其它goroutine
也感知到了o.done
等于1,不會重復(fù)執(zhí)行f
函數(shù)了
總結(jié)
上文就是針對Go源碼sync.Once
原理和使用方式的講解。在實(shí)際開發(fā)中,sync.Once
的使用還是非常普遍的。掌握sync.Once的底層原理,有助于我們在今后的開發(fā)中更有把握地利用它永遠(yuǎn)只執(zhí)行一次函數(shù)的特性,完成復(fù)雜的技術(shù)需求或者業(yè)務(wù)需求。
到此這篇關(guān)于Go中sync.Once源碼的深度講解的文章就介紹到這了,更多相關(guān)Go sync.Once內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
golang gorm 結(jié)構(gòu)體的表字段缺省值設(shè)置方式
這篇文章主要介紹了golang gorm 結(jié)構(gòu)體的表字段缺省值設(shè)置方式,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-12-12