go sync Once實(shí)現(xiàn)原理示例解析
正文
在很多情況下,我們可能需要控制某一段代碼只執(zhí)行一次,比如做某些初始化操作,如初始化數(shù)據(jù)庫(kù)連接等。 對(duì)于這種場(chǎng)景,go 為我們提供了 sync.Once 對(duì)象,它保證了某個(gè)動(dòng)作只被執(zhí)行一次。 當(dāng)然我們也是可以自己通過(guò) Mutex 實(shí)現(xiàn) sync.Once 的功能,但是相比來(lái)說(shuō)繁瑣了那么一點(diǎn), 因?yàn)槲覀儾粌H要自己去控制鎖,還要通過(guò)一個(gè)標(biāo)識(shí)來(lái)標(biāo)志是否已經(jīng)執(zhí)行過(guò)。
Once 的實(shí)現(xiàn)
Once 的實(shí)現(xiàn)非常簡(jiǎn)單,如下,就只有 20 來(lái)行代碼,但里面包含了 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()
}
}
簡(jiǎn)要說(shuō)明:
done字段指示了操作是否已執(zhí)行,也就是我們傳遞給Do的函數(shù)是否已經(jīng)被執(zhí)行。Do方法接收一個(gè)函數(shù)參數(shù),這個(gè)函數(shù)參數(shù)只會(huì)被執(zhí)行一次。Once內(nèi)部是通過(guò)Mutex來(lái)實(shí)現(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 會(huì)調(diào)用 3 次,但最終只會(huì)執(zhí)行一次
once.Do(test)
wg.Done()
}()
}
wg.Wait()
fmt.Println(a) // 1
}
Once 的一些工作機(jī)制
Once的Do方法可以保證,在多個(gè) goroutine 同時(shí)執(zhí)行Do方法的時(shí)候, 在第一個(gè)搶占到Do執(zhí)行權(quán)的 goroutine 執(zhí)行返回之前,其他 goroutine 都會(huì)阻塞在Once.Do的調(diào)用上, 只有第一個(gè)Do調(diào)用返回的時(shí)候,其他 goroutine 才可以繼續(xù)執(zhí)行下去,并且其他所有的 goroutine 不會(huì)再執(zhí)行傳遞給Do的函數(shù)。(如果是初始化的場(chǎng)景,這可以避免尚未初始化完成就執(zhí)行其他的操作)- 如果
Once.Do發(fā)生panic的時(shí)候,傳遞給Do的函數(shù)依然被標(biāo)記為已完成。后續(xù)對(duì)Do的調(diào)用也不會(huì)再執(zhí)行傳給Do的函數(shù)參數(shù)。 - 我們不能簡(jiǎn)單地通過(guò)
atomic.CompareAndSwapUint32來(lái)決定是否執(zhí)行f(),因?yàn)樵诙鄠€(gè) goroutine 同時(shí)執(zhí)行的時(shí)候,它無(wú)法保證f()只被執(zhí)行一次。所以Once里面用了Mutex,這樣就可以有效地保護(hù)臨界區(qū)。
// 錯(cuò)誤實(shí)現(xiàn),這不能保證 f 只被執(zhí)行一次
if atomic.CompareAndSwapUint32(&o.done, 0, 1) {
f()
}
Once.Do的函數(shù)參數(shù)是沒(méi)有參數(shù)的,如果我們需要傳遞一些參數(shù),可以再對(duì)f做一層包裹。
config.once.Do(func() { config.init(filename) })
Once 詳解
hotpath
這里說(shuō)的 hotpath 指的是 Once 里的第一個(gè)字段 done:
type Once struct {
// hotpath
done uint32
m Mutex
}
Once 結(jié)構(gòu)體的第一個(gè)字段是 done,這是因?yàn)?done 的訪問(wèn)是遠(yuǎn)遠(yuǎn)大于 Once 中另外一個(gè)字段 m 的, 放在第一個(gè)字段中,編譯器就可以做一些優(yōu)化,因?yàn)榻Y(jié)構(gòu)體的地址其實(shí)就是結(jié)構(gòu)體第一個(gè)字段的地址, 這樣一來(lái),在訪問(wèn) done 字段的時(shí)候,就不需要通過(guò)結(jié)構(gòu)體地址 + 偏移量的方式來(lái)訪問(wèn), 這在一定程度上提高了性能。
結(jié)構(gòu)體地址計(jì)算示例:
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 的地址也可以通過(guò):結(jié)構(gòu)體地址 + age 字段偏移量 計(jì)算得出。
// 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 方法中,是通過(guò) atomic.LoadUint32 的方式來(lái)判斷 done 是否等于 0 的, 這是因?yàn)?,如果直接使?done == 0 的方式的話,就有可能導(dǎo)致在 doSlow 里面對(duì) done 設(shè)置為 1 之后, 在 Do 方法里面無(wú)法正常觀測(cè)到。因此用了 atomic.LoadUint32。
而在 doSlow 里面是可以通過(guò) done == 0 來(lái)判斷的,這是因?yàn)?doSlow 里面已經(jīng)通過(guò) Mutex 保護(hù)起來(lái)了。 唯一設(shè)置 done = 1 的地方就在臨界區(qū)里面,所以 doSlow 里面通過(guò) done == 0 來(lái)判斷是完全沒(méi)有問(wèn)題的。
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 方法中,設(shè)置 done 為 1 也是通過(guò) atomic.StoreUint32 來(lái)設(shè)置的。 這樣就可以保證在設(shè)置了 done 為 1 之后,可以及時(shí)被其他 goroutine 看到。
Mutex
doSlow 的實(shí)現(xiàn)里面,最終還是要通過(guò) Mutex 來(lái)保護(hù)臨界區(qū), 通過(guò) Mutex 可以實(shí)現(xiàn) f 只被執(zhí)行一次,并且其他的 goroutine 都可以使用這一次 f 的執(zhí)行結(jié)果。 因?yàn)槠渌?goroutine 在第一次 f 調(diào)用未返回之前,都阻塞在獲取 Mutex 鎖的地方, 當(dāng)它們獲取到 Mutex 鎖的時(shí)候,得以繼續(xù)往下執(zhí)行,但這個(gè)時(shí)候 f 已經(jīng)執(zhí)行完畢了, 所以當(dāng)它們獲取到 Mutex 鎖之后其實(shí)什么也沒(méi)有干。
但是它們的阻塞狀態(tài)被解除了,可以繼續(xù)往下執(zhí)行。
總結(jié)
Once保證了傳入的函數(shù)只會(huì)執(zhí)行一次,這常常用在一些初始化的場(chǎng)景、或者單例模式。Once可以保證所有對(duì)Do的并發(fā)調(diào)用都是安全的,所有對(duì)Once.Do調(diào)用之后的操作,一定會(huì)在第一次對(duì)f調(diào)用之后執(zhí)行。(沒(méi)有獲取到f執(zhí)行權(quán)的 goroutine 會(huì)阻塞)- 即使
Once.Do里面的f出現(xiàn)了panic,后續(xù)也不會(huì)再次調(diào)用f。
以上就是go sync Once實(shí)現(xiàn)原理示例解析的詳細(xì)內(nèi)容,更多關(guān)于go sync Once實(shí)現(xiàn)原理的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
使用docker構(gòu)建golang線上部署環(huán)境的步驟詳解
這篇文章主要介紹了使用docker構(gòu)建golang線上部署環(huán)境的步驟,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧。2017-11-11
Golang中漏洞數(shù)據(jù)庫(kù)的使用詳解
govulncheck是Golang中的漏洞掃描工具,它強(qiáng)大功能的背后,離不開?Go?漏洞數(shù)據(jù)庫(kù)(Go?vulnerability?database)的支持,所以本文就來(lái)為大家詳細(xì)講解下?Go?漏洞數(shù)據(jù)庫(kù)相關(guān)的知識(shí)2023-09-09
go語(yǔ)言Timer計(jì)時(shí)器的用法示例詳解
Go語(yǔ)言的標(biāo)準(zhǔn)庫(kù)里提供兩種類型的計(jì)時(shí)器Timer和Ticker。這篇文章通過(guò)實(shí)例代碼給大家介紹go語(yǔ)言Timer計(jì)時(shí)器的用法,代碼簡(jiǎn)單易懂,感興趣的朋友跟隨小編一起看看吧2020-05-05
利用Go語(yǔ)言實(shí)現(xiàn)簡(jiǎn)單Ping過(guò)程的方法
相信利用各種語(yǔ)言實(shí)現(xiàn)Ping已經(jīng)是大家喜聞樂(lè)見的事情了,網(wǎng)絡(luò)上利用Golang實(shí)現(xiàn)Ping已經(jīng)有比較詳細(xì)的代碼示例,但大多是僅僅是實(shí)現(xiàn)了Request過(guò)程,而對(duì)Response的回顯內(nèi)容并沒(méi)有做接收。而Ping程序不僅僅是發(fā)送一個(gè)ICMP,更重要的是如何接收并進(jìn)行統(tǒng)計(jì)。2016-09-09
Go項(xiàng)目編寫Makefile規(guī)則文件概述
這篇文章主要為大家介紹了Go項(xiàng)目編寫Makefile文件規(guī)則概述,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步早日升職加薪2022-04-04
go-micro微服務(wù)JWT跨域認(rèn)證問(wèn)題
JWT 以 JSON 對(duì)象的形式安全傳遞信息。因?yàn)榇嬖跀?shù)字簽名,因此所傳遞的信息是安全的,這篇文章主要介紹了go-micro微服務(wù)JWT跨域認(rèn)證,需要的朋友可以參考下2023-01-01
Golang實(shí)現(xiàn)解析JSON的三種方法總結(jié)
這篇文章主要為大家詳細(xì)介紹了Golang實(shí)現(xiàn)解析JSON的三種方法,文中的示例代碼講解詳細(xì),對(duì)我們學(xué)習(xí)了解JSON有一定幫助,需要的可以參考一下2022-09-09

