Go并發(fā)編程之sync.Once使用實例詳解
一.序
單從庫名大概就能猜出其作用。sync.Once
使用起來很簡單, 下面是一個簡單的使用案例
package main import ( "fmt" "sync" ) func main() { var ( once sync.Once wg sync.WaitGroup ) for i := 0; i < 10; i++ { wg.Add(1) // 這里要注意講i顯示的當參數(shù)傳入內部的匿名函數(shù) go func(i int) { defer wg.Done() // fmt.Println("once", i) once.Do(func() { fmt.Println("once", i) }) }(i) } wg.Wait() fmt.Printf("over") }
輸出:
❯ go run ./demo.go
once 9
測試如果不添加once.Do
這段代碼,則會輸出如下結果,并且每次執(zhí)行的輸出都不一樣。
once 9
once 0
once 3
once 6
once 4
once 1
once 5
once 2
once 7
once 8
從兩次輸出不同,我們可以得知 sync.Once
的作用是:保證傳入的函數(shù)
只執(zhí)行一次
二. 源碼分析
2.1結構體
Once的結構體如下
type Once struct { done uint32 m Mutex }
每一個 sync.Once 結構體中都只包含一個用于標識代碼塊是否執(zhí)行過的 done 以及一個互斥鎖 sync.Mutex
2.2 接口
sync.Once.Do
是 sync.Once
結構體對外唯一暴露的方法,該方法會接收一個入?yún)榭盏暮瘮?shù):
- 如果傳入的函數(shù)已經(jīng)執(zhí)行過,會直接返回
- 如果傳入的函數(shù)沒有執(zhí)行過, 會調用
sync.Once.doSlow
執(zhí)行傳入的參數(shù)
func (o *Once) Do(f func()) { // Note: Here is an incorrect implementation of Do: // // if atomic.CompareAndSwapUint32(&o.done, 0, 1) { // f() // } // // Do guarantees that when it returns, f has finished. // This implementation would not implement that guarantee: // given two simultaneous calls, the winner of the cas would // call f, and the second would return immediately, without // waiting for the first's call to f to complete. // This is why the slow path falls back to a mutex, and why // the atomic.StoreUint32 must be delayed until after f returns. if atomic.LoadUint32(&o.done) == 0 { // Outlined slow-path to allow inlining of the fast-path. o.doSlow(f) } }
代碼注釋中特別給了一個說明: 很容易犯錯的一種實現(xiàn)
if atomic.CompareAndSwapUint32(&o.done, 0, 1) { f() }
如果這么實現(xiàn)最大的問題是,如果并發(fā)調用,一個 goroutine 執(zhí)行,另外一個不會等正在執(zhí)行的這個成功之后返回,而是直接就返回了,這就不能保證傳入的方法一定會先執(zhí)行一次了
正確的實現(xiàn)方式
if atomic.LoadUint32(&o.done) == 0 { // Outlined slow-path to allow inlining of the fast-path. o.doSlow(f) }
會先判斷 done 是否為 0,如果不為 0 說明還沒執(zhí)行過,就進入 doSlow
func (o *Once) doSlow(f func()) { o.m.Lock() defer o.m.Unlock() if o.done == 0 { defer atomic.StoreUint32(&o.done, 1) f() } }
在 doSlow
當中使用了互斥鎖來保證只會執(zhí)行一次
具體的邏輯
- 為當前Goroutine獲取互斥鎖
- 執(zhí)行傳入的無入?yún)⒑瘮?shù);
- 運行延遲函數(shù), 將成員變量
done
更新為1
三. 使用場景案例
3.1 單例模式
原子操作配合互斥鎖可以實現(xiàn)非常高效的單件模式?;コ怄i的代價比普通整數(shù)的原子讀寫高很多,在性能敏感的地方可以增加一個數(shù)字型的標志位,通過原子檢測標志位狀態(tài)降低互斥鎖的使用次數(shù)來提高性能。
type singleton struct {} var ( instance *singleton initialized uint32 mu sync.Mutex ) func Instance() *singleton { if atomic.LoadUint32(&initialized) == 1 { return instance } mu.Lock() defer mu.Unlock() if instance == nil { defer atomic.StoreUint32(&initialized, 1) instance = &singleton{} } return instance }
而使用sync.Once
能更簡單實現(xiàn)單例模式
type singleton struct {} var ( instance *singleton once sync.Once ) func Instance() *singleton { once.Do(func() { instance = &singleton{} }) return instance }
3.2 加載配置文件示例
延遲一個開銷很大的初始化操作到真正用到它的時候再執(zhí)行是一個很好的實踐。因為預先初始化一個變量(比如在init函數(shù)中完成初始化)會增加程序的啟動耗時,而且有可能實際執(zhí)行過程中這個變量沒有用上,那么這個初始化操作就不是必須要做的。我們來看一個例子:
var icons map[string]image.Image func loadIcons() { icons = map[string]image.Image{ "left": loadIcon("left.png"), "up": loadIcon("up.png"), "right": loadIcon("right.png"), "down": loadIcon("down.png"), } } // Icon 被多個goroutine調用時不是并發(fā)安全的 // 因為map類型本就不是類型安全數(shù)據(jù)結構 func Icon(name string) image.Image { if icons == nil { loadIcons() } return icons[name] }
多個goroutine并發(fā)調用Icon函數(shù)時不是并發(fā)安全的,編譯器和CPU可能會在保證每個goroutine都滿足串行一致的基礎上自由地重排訪問內存的順序。loadIcons函數(shù)可能會被重排為以下結果:
func loadIcons() {
icons = make(map[string]image.Image)
icons["left"] = loadIcon("left.png")
icons["up"] = loadIcon("up.png")
icons["right"] = loadIcon("right.png")
icons["down"] = loadIcon("down.png")
}
在這種情況下就會出現(xiàn)即使判斷了icons不是nil也不意味著變量初始化完成了??紤]到這種情況,我們能想到的辦法就是添加互斥鎖,保證初始化icons的時候不會被其他的goroutine操作,但是這樣做又會引發(fā)性能問題。
可以使用sync.Once
改造代碼
var icons map[string]image.Image var loadIconsOnce sync.Once func loadIcons() { icons = map[string]image.Image{ "left": loadIcon("left.png"), "up": loadIcon("up.png"), "right": loadIcon("right.png"), "down": loadIcon("down.png"), } } // Icon 是并發(fā)安全的,并且保證了在代碼運行的時候才會加載配置 func Icon(name string) image.Image { loadIconsOnce.Do(loadIcons) return icons[name] }
這樣設計就能保證初始化操作的時候是并發(fā)安全的并且初始化操作也不會被執(zhí)行多次。
四.總結
作為用于保證函數(shù)執(zhí)行次數(shù)的 sync.Once
結構體,它使用互斥鎖和 sync/atomic
包提供的方法實現(xiàn)了某個函數(shù)在程序運行期間只能執(zhí)行一次的語義。在使用該結構體時,我們也需要注意以下的問題:
sync.Once.Do
方法中傳入的函數(shù)只會被執(zhí)行一次,哪怕函數(shù)中發(fā)生了 panic;- 兩次調用
sync.Once.Do
方法傳入不同的函數(shù)只會執(zhí)行第一次調傳入的函數(shù);
五. 參考
- https://lailin.xyz/post/go-training-week3-once.html
- https://www.topgoer.cn/docs/gozhuanjia/chapter055.2-waitgroup
- https://www.topgoer.com/并發(fā)編程/sync.html
- https://chai2010.cn/advanced-go-programming-book/ch1-basic/ch1-05-mem.html
到此這篇關于Go并發(fā)編程--sync.Once的文章就介紹到這了,更多相關Go并發(fā)編程內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
go內存緩存BigCache實現(xiàn)BytesQueue源碼解讀
這篇文章主要為大家介紹了go內存緩存BigCache實現(xiàn)BytesQueue源碼解讀,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-09-09GoFrame框架garray對比PHP的array優(yōu)勢
這篇文章主要為大家介紹了GoFrame框架garray對比PHP的array優(yōu)勢詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-06-06詳解Golang中創(chuàng)建error的方式總結與應用場景
Golang中創(chuàng)建error的方式包括errors.New、fmt.Errorf、自定義實現(xiàn)了error接口的類型等,本文主要為大家介紹了這些方式的具體應用場景,需要的可以參考一下2023-07-07