go sync Once實現(xiàn)原理示例解析
正文
在很多情況下,我們可能需要控制某一段代碼只執(zhí)行一次,比如做某些初始化操作,如初始化數(shù)據(jù)庫連接等。 對于這種場景,go 為我們提供了 sync.Once
對象,它保證了某個動作只被執(zhí)行一次。 當然我們也是可以自己通過 Mutex
實現(xiàn) sync.Once
的功能,但是相比來說繁瑣了那么一點, 因為我們不僅要自己去控制鎖,還要通過一個標識來標志是否已經(jīng)執(zhí)行過。
Once 的實現(xiàn)
Once
的實現(xiàn)非常簡單,如下,就只有 20 來行代碼,但里面包含了 go 并發(fā)、同步的一些常見處理方法。
package sync import ( "sync/atomic" ) type Once struct { done uint32 m Mutex } func (o *Once) Do(f func()) { if atomic.LoadUint32(&o.done) == 0 { o.doSlow(f) } } func (o *Once) doSlow(f func()) { o.m.Lock() defer o.m.Unlock() if o.done == 0 { defer atomic.StoreUint32(&o.done, 1) f() } }
簡要說明:
done
字段指示了操作是否已執(zhí)行,也就是我們傳遞給Do
的函數(shù)是否已經(jīng)被執(zhí)行。Do
方法接收一個函數(shù)參數(shù),這個函數(shù)參數(shù)只會被執(zhí)行一次。Once
內(nèi)部是通過Mutex
來實現(xiàn)不同協(xié)程之間的同步的。
使用示例
在下面的例子中,once.Do(test)
被執(zhí)行了 3 次,但是最終 test
只被執(zhí)行了一次。
package sync import ( "fmt" "sync" "testing" ) var once sync.Once var a = 0 func test() { a++ } func TestOnce(t *testing.T) { var wg sync.WaitGroup wg.Add(3) for i := 0; i < 3; i++ { go func() { // once.Do 會調(diào)用 3 次,但最終只會執(zhí)行一次 once.Do(test) wg.Done() }() } wg.Wait() fmt.Println(a) // 1 }
Once 的一些工作機制
Once
的Do
方法可以保證,在多個 goroutine 同時執(zhí)行Do
方法的時候, 在第一個搶占到Do
執(zhí)行權(quán)的 goroutine 執(zhí)行返回之前,其他 goroutine 都會阻塞在Once.Do
的調(diào)用上, 只有第一個Do
調(diào)用返回的時候,其他 goroutine 才可以繼續(xù)執(zhí)行下去,并且其他所有的 goroutine 不會再執(zhí)行傳遞給Do
的函數(shù)。(如果是初始化的場景,這可以避免尚未初始化完成就執(zhí)行其他的操作)- 如果
Once.Do
發(fā)生panic
的時候,傳遞給Do
的函數(shù)依然被標記為已完成。后續(xù)對Do
的調(diào)用也不會再執(zhí)行傳給Do
的函數(shù)參數(shù)。 - 我們不能簡單地通過
atomic.CompareAndSwapUint32
來決定是否執(zhí)行f()
,因為在多個 goroutine 同時執(zhí)行的時候,它無法保證f()
只被執(zhí)行一次。所以Once
里面用了Mutex
,這樣就可以有效地保護臨界區(qū)。
// 錯誤實現(xiàn),這不能保證 f 只被執(zhí)行一次 if atomic.CompareAndSwapUint32(&o.done, 0, 1) { f() }
Once.Do
的函數(shù)參數(shù)是沒有參數(shù)的,如果我們需要傳遞一些參數(shù),可以再對f
做一層包裹。
config.once.Do(func() { config.init(filename) })
Once 詳解
hotpath
這里說的 hotpath
指的是 Once
里的第一個字段 done
:
type Once struct { // hotpath done uint32 m Mutex }
Once
結(jié)構(gòu)體的第一個字段是 done
,這是因為 done
的訪問是遠遠大于 Once
中另外一個字段 m
的, 放在第一個字段中,編譯器就可以做一些優(yōu)化,因為結(jié)構(gòu)體的地址其實就是結(jié)構(gòu)體第一個字段的地址, 這樣一來,在訪問 done
字段的時候,就不需要通過結(jié)構(gòu)體地址 + 偏移量的方式來訪問, 這在一定程度上提高了性能。
結(jié)構(gòu)體地址計算示例:
type person struct { name string age int } func TestStruct(t *testing.T) { var p = person{ name: "foo", age: 10, } // p 和 p.name 的地址相同 // 0xc0000100a8, 0xc0000100a8 fmt.Printf("%p, %p\n", &p, &p.name) // p.age 的地址 // 0xc0000100b8 fmt.Printf("%p\n", &p.age) // p.age 的地址也可以通過:結(jié)構(gòu)體地址 + age 字段偏移量 計算得出。 // 0xc0000100b8 fmt.Println(unsafe.Add(unsafe.Pointer(&p), unsafe.Offsetof(p.age))) }
atomic.LoadUint32
func (o *Once) Do(f func()) { if atomic.LoadUint32(&o.done) == 0 { o.doSlow(f) } }
在 Do
方法中,是通過 atomic.LoadUint32
的方式來判斷 done
是否等于 0 的, 這是因為,如果直接使用 done == 0
的方式的話,就有可能導致在 doSlow
里面對 done
設置為 1 之后, 在 Do
方法里面無法正常觀測到。因此用了 atomic.LoadUint32
。
而在 doSlow
里面是可以通過 done == 0
來判斷的,這是因為 doSlow
里面已經(jīng)通過 Mutex
保護起來了。 唯一設置 done = 1
的地方就在臨界區(qū)里面,所以 doSlow
里面通過 done == 0
來判斷是完全沒有問題的。
atomic.StoreUint32
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
方法中,設置 done
為 1 也是通過 atomic.StoreUint32
來設置的。 這樣就可以保證在設置了 done
為 1 之后,可以及時被其他 goroutine 看到。
Mutex
doSlow
的實現(xiàn)里面,最終還是要通過 Mutex
來保護臨界區(qū), 通過 Mutex
可以實現(xiàn) f
只被執(zhí)行一次,并且其他的 goroutine 都可以使用這一次 f
的執(zhí)行結(jié)果。 因為其他 goroutine 在第一次 f
調(diào)用未返回之前,都阻塞在獲取 Mutex
鎖的地方, 當它們獲取到 Mutex
鎖的時候,得以繼續(xù)往下執(zhí)行,但這個時候 f
已經(jīng)執(zhí)行完畢了, 所以當它們獲取到 Mutex
鎖之后其實什么也沒有干。
但是它們的阻塞狀態(tài)被解除了,可以繼續(xù)往下執(zhí)行。
總結(jié)
Once
保證了傳入的函數(shù)只會執(zhí)行一次,這常常用在一些初始化的場景、或者單例模式。Once
可以保證所有對Do
的并發(fā)調(diào)用都是安全的,所有對Once.Do
調(diào)用之后的操作,一定會在第一次對f
調(diào)用之后執(zhí)行。(沒有獲取到f
執(zhí)行權(quán)的 goroutine 會阻塞)- 即使
Once.Do
里面的f
出現(xiàn)了panic
,后續(xù)也不會再次調(diào)用f
。
以上就是go sync Once實現(xiàn)原理示例解析的詳細內(nèi)容,更多關于go sync Once實現(xiàn)原理的資料請關注腳本之家其它相關文章!
相關文章
使用docker構(gòu)建golang線上部署環(huán)境的步驟詳解
這篇文章主要介紹了使用docker構(gòu)建golang線上部署環(huán)境的步驟,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧。2017-11-11Golang實現(xiàn)解析JSON的三種方法總結(jié)
這篇文章主要為大家詳細介紹了Golang實現(xiàn)解析JSON的三種方法,文中的示例代碼講解詳細,對我們學習了解JSON有一定幫助,需要的可以參考一下2022-09-09