Go中sync.Once源碼的深度講解
概念
sync.Once
是Go語言標準庫中的一個同步原語,用于確保某個操作只執(zhí)行一次。它在多線程環(huán)境中非常有用,尤其是在需要初始化共享資源或執(zhí)行某些一次性任務時。
簡單示例
當我們在web服務訪問某個路由時,如果需要事先獲取某些配置,往往會寫一個loadConfig
函數(shù),獲取一個cfg
配置項。多次路由訪問所需要獲取的配置項通常是相同的,如果對于每次路由訪問,都加載一次loadConfig
函數(shù),會導致產(chǎn)生一些不必要的開銷。如果loadConfig
涉及到讀取文件、解析配置、網(wǎng)絡請求時,有可能會額外增加的請求響應時間,降低服務的吞吐量。使用sync.Once
包提供的Do
函數(shù),就可以只在第一次請求時調(diào)用loadConfig
函數(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) // 僅第一次訪問時會執(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í)行標識位,0-未執(zhí)行 1-已執(zhí)行 m Mutex // 互斥鎖,確保并發(fā)安全 } func (o *Once) Do(f func()) { // 第一次執(zhí)行Do函數(shù)時,原子操作檢查o.done==0,執(zhí)行doSlow函數(shù)后,o.done==1 // 第二次及之后執(zhí)行Do函數(shù),原子操作檢查o.done標識位為1,Do函數(shù)不執(zhí)行任何功能,確保了f函數(shù)只在第一次被執(zhí)行 if o.done.Load() == 0 { o.doSlow(f) // 調(diào)用doSlow函數(shù)執(zhí)行f方法。第一次執(zhí)行時,同一時間可能有多個goroutine嘗試同時執(zhí)行doSlow函數(shù) } } func (o *Once) doSlow(f func()) { o.m.Lock() // 加鎖保護,避免多個goroutine同時繞過之前的原子操作檢查,并發(fā)修改o.done的值 defer o.m.Unlock() // 二次檢查o.done的值,同一時間并發(fā)執(zhí)行doSlow函數(shù)的goroutine,在第一個goroutine將o.done置為1并解除互斥鎖后, // 剩下的goroutine識別到自身的o.done已經(jīng)被設為1,無法繞過二次檢查 if o.done.Load() == 0 { defer o.done.Store(1) // 需要在f()函數(shù)執(zhí)行完成之后,原子性地將o.done設為1 f() // 執(zhí)行f方法,一定只有一個goroutine會調(diào)用這個方法 } }
可以看到,once.go
文件的代碼非常精煉。僅定義了一個含2個非導出字段done
和m
的結構體Once
,并提供了一個doSlow
方法用于執(zhí)行f
函數(shù)。當我們調(diào)用Do
方法時,程序經(jīng)歷了幾個關鍵步驟:
- 判斷
done
標志位是否等于0,如果是,說明f
函數(shù)還沒有被執(zhí)行,執(zhí)行doSlow
方法 mu
互斥鎖加鎖,防止多個goroutine
并發(fā)操作- double-check
done
標志位是否等于0,如果是,說明f
函數(shù)還沒有被執(zhí)行,執(zhí)行f
函數(shù) f
函數(shù)執(zhí)行完成之后,再將done
標志位原子性設為1。使用原子操作是從內(nèi)存可見性的角度出發(fā),如果done
使用uint32
而不是atomic.Uint32
,done
修改可能不會立即被其它goroutine
感知,解鎖后仍有可能存在goroutine
的done
等于0,重復執(zhí)行f
函數(shù)mu
互斥鎖解鎖。此時進入到doSlow
函數(shù)的其它goroutine
也感知到了o.done
等于1,不會重復執(zhí)行f
函數(shù)了
總結
上文就是針對Go源碼sync.Once
原理和使用方式的講解。在實際開發(fā)中,sync.Once
的使用還是非常普遍的。掌握sync.Once的底層原理,有助于我們在今后的開發(fā)中更有把握地利用它永遠只執(zhí)行一次函數(shù)的特性,完成復雜的技術需求或者業(yè)務需求。
到此這篇關于Go中sync.Once源碼的深度講解的文章就介紹到這了,更多相關Go sync.Once內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!