淺析GO并發(fā)處理選擇sync還是channel
如何選擇 sync 和 channel
以前寫 C 的時(shí)候,我們一般是都通過共享內(nèi)存來通信,對于并發(fā)去操作某一塊數(shù)據(jù)時(shí),為了保證數(shù)據(jù)安全,控制線程間同步,我們們會(huì)去使用互斥鎖,加鎖解鎖來進(jìn)行處理
然而 GO 語言中建議的時(shí)候通過通信來共享內(nèi)存,使用 channel 來完成臨界區(qū)的同步機(jī)制
可是 GO 語言中的 channel 畢竟是屬于比較高級(jí)的原語,自然在性能上就比不上 sync包里面的鎖機(jī)制,感興趣的同學(xué)可以自己寫一個(gè)簡單的基準(zhǔn)測試來確認(rèn)一下效果,評(píng)論去可以交流
另外,使用 sync 包來控制同步時(shí),我們不會(huì)失去結(jié)構(gòu)對象的所有權(quán),還能讓多個(gè)協(xié)程之間同步訪問臨界區(qū)的資源,那么如果我們的需求能夠符合這種情況時(shí),還是建議使用 sync 包來控制同步更加的合理和高效
為什么會(huì)選擇使用 sync 包來控制同步結(jié)論:
- 不期望失去結(jié)構(gòu)的控制權(quán)的同時(shí),還期望多個(gè)協(xié)程能夠安全的同步訪問臨界區(qū)資源
- 對性能要求會(huì)更高的情況
sync 的 Mutex 和 RWMutex
查看 sync 包的源碼(xxx\Go\src\sync
),我們可以看到 sync 包下面有如下幾個(gè)結(jié)構(gòu):
- Mutex
- RWMutex
- Once
- Cond
- Pool
- atomic 包原子操作
上述經(jīng)常使用的就是 Mutex 了,尤其是最開始不善于使用 channel 的時(shí)候,覺得使用 Mutex 非常的順手,其次 RWMutex 相對來說就會(huì)用的少一些
不知大家有沒有關(guān)注過,使用 Mutex 和 使用 RWMutex 的性能表現(xiàn),獲取大部分人都是默認(rèn)使用互斥鎖,一起寫個(gè) demo 來看看 他倆的性能對比
var ( mu sync.Mutex murw sync.RWMutex tt1 = 1 tt2 = 2 tt3 = 3 ) // 使用 Mutex 控制讀取數(shù)據(jù) func BenchmarkReadMutex(b *testing.B) { b.RunParallel(func(pp *testing.PB) { for pp.Next() { mu.Lock() _ = tt1 mu.Unlock() } }) } // 使用 RWMutex 控制讀取數(shù)據(jù) func BenchmarkReadRWMutex(b *testing.B) { b.RunParallel(func(pp *testing.PB) { for pp.Next() { murw.RLock() _ = tt2 murw.RUnlock() } }) } // 使用 RWMutex 控制讀寫入數(shù)據(jù) func BenchmarkWriteRWMutex(b *testing.B) { b.RunParallel(func(pp *testing.PB) { for pp.Next() { murw.Lock() tt3++ murw.Unlock() } }) }
寫了三個(gè)簡單的基準(zhǔn)測試
- 使用互斥鎖讀取數(shù)據(jù)
- 使用讀寫鎖的讀鎖讀取數(shù)據(jù)
- 使用讀寫鎖讀取和寫入數(shù)據(jù)
$ go test -bench . bbb_test.go --cpu 2 goos: windows goarch: amd64 cpu: Intel(R) Core(TM)2 Duo CPU T7700 @ 2.40GHz BenchmarkReadMutex-2 39638757 30.45 ns/op BenchmarkReadRWMutex-2 43082371 26.97 ns/op BenchmarkWriteRWMutex-2 16383997 71.35 ns/op $ go test -bench . bbb_test.go --cpu 4 goos: windows goarch: amd64 cpu: Intel(R) Core(TM)2 Duo CPU T7700 @ 2.40GHz BenchmarkReadMutex-4 17066666 73.47 ns/op BenchmarkReadRWMutex-4 43885633 30.33 ns/op BenchmarkWriteRWMutex-4 10593098 110.3 ns/op $ go test -bench . bbb_test.go --cpu 8 goos: windows goarch: amd64 cpu: Intel(R) Core(TM)2 Duo CPU T7700 @ 2.40GHz BenchmarkReadMutex-8 8969340 129.0 ns/op BenchmarkReadRWMutex-8 36451077 33.46 ns/op BenchmarkWriteRWMutex-8 7728303 158.5 ns/op $ go test -bench . bbb_test.go --cpu 16 goos: windows goarch: amd64 cpu: Intel(R) Core(TM)2 Duo CPU T7700 @ 2.40GHz BenchmarkReadMutex-16 8533333 132.6 ns/op BenchmarkReadRWMutex-16 39638757 29.98 ns/op BenchmarkWriteRWMutex-16 6751646 173.9 ns/op $ go test -bench . bbb_test.go --cpu 128 goos: windows goarch: amd64 cpu: Intel(R) Core(TM)2 Duo CPU T7700 @ 2.40GHz BenchmarkReadMutex-128 10155368 116.0 ns/op BenchmarkReadRWMutex-128 35108558 33.27 ns/op BenchmarkWriteRWMutex-128 6334021 195.3 ns/op
可以看出來當(dāng)并發(fā)較小的時(shí)候,使用互斥鎖和使用讀寫鎖的讀鎖性能類似,當(dāng)并發(fā)逐漸變大時(shí),讀寫鎖的讀鎖性能并未發(fā)生較大變化,互斥鎖和讀寫鎖的性能都會(huì)隨著并發(fā)的變大而下降
那么很明顯,讀寫鎖適用于讀多寫少的場景,在大并發(fā)讀書數(shù)據(jù)的時(shí)候,多個(gè)協(xié)程可以同時(shí)拿到讀鎖,減少鎖競爭和等待時(shí)間
而互斥鎖并發(fā)的時(shí)候,多個(gè)協(xié)程中,只有一個(gè)協(xié)程能拿到鎖,其他協(xié)程就會(huì)阻塞和等待,影響性能
舉個(gè)例子,我們正常使用互斥鎖,看看可能會(huì)出現(xiàn)什么樣的問題
使用 sync 需要注意的地方
平時(shí)使用 sync 包中的鎖的時(shí)候,需要注意的是不要去拷貝已經(jīng)已經(jīng)使用過的 Mutex 或者是 RWMutex
寫一個(gè)簡單的 demo:
var mu sync.Mutex // sync 的互斥鎖,讀寫鎖,在被使用之后,就不要去復(fù)制這個(gè)對象,若要復(fù)制,需要在其未被使用的時(shí)候 func main() { go func(mm sync.Mutex) { for { mm.Lock() time.Sleep(time.Second * 1) fmt.Println("g2") mm.Unlock() } }(mu) mu.Lock() go func(mm sync.Mutex) { for { mm.Lock() time.Sleep(time.Second * 1) fmt.Println("g3") mm.Unlock() } }(mu) time.Sleep(time.Second * 1) fmt.Println("g1") mu.Unlock() time.Sleep(time.Second * 20) }
感興趣的朋友的,可以運(yùn)行一下,可以看到打印的結(jié)果中時(shí)沒有 g3
的,因此 g3
所在的協(xié)程已經(jīng)發(fā)生了死鎖,沒有機(jī)會(huì)去調(diào)用 unlock
出現(xiàn)這種情況的原因是這樣的,先來看看 Mutex 的內(nèi)部結(jié)構(gòu):
//... // A Mutex must not be copied after first use. //... type Mutex struct { state int32 sema uint32 }
因?yàn)槔?Mutex 中的內(nèi)部結(jié)構(gòu)是有一個(gè) state (表示互斥鎖的狀態(tài))和 sema(表示控制互斥鎖的信號(hào)量),其中初始化 Mutex 的時(shí)候,他們都是 0,但是當(dāng)我們用 Mutex 加鎖時(shí),Mutex 的狀態(tài)就變成了 Locked 的狀態(tài),這個(gè)時(shí)候,其中一個(gè)協(xié)程去拷貝這個(gè) Mutex,并在自己協(xié)程中加鎖,就會(huì)出現(xiàn)死鎖的情況,這一點(diǎn)是非常需要注意的
如果涉及到這種多個(gè)協(xié)程使用 Mutex 的情況, 可以使用閉包或者傳入包裹鎖的結(jié)構(gòu)地址或者指針,這樣就可以避免使用鎖的時(shí)候?qū)е虏豢深A(yù)期的結(jié)果,避免一臉蒙圈
sync.Once
sync 包中的其他成員,不知 xdm 使用的多么,相對使用頻率較高的應(yīng)該就是 sync.Once 了,其他成員 xdm 可以自行看看源碼,或者評(píng)論區(qū)留言哦,我們來看看 syn.Once 如何使用,都有哪些需要注意的?
還記得之前寫 C 或者 C++ 的時(shí)候,對于程序生命周期只有一個(gè)實(shí)例的時(shí)候,我們會(huì)選擇使用單例模式來進(jìn)行處理,那么此處的 sync.Once 就是非常適合用在單例模式中
sync.Once 可以保證任意一個(gè)函數(shù)在程序運(yùn)行期間只被執(zhí)行一次,這一點(diǎn)相對來說就比每個(gè)包中的 init 函數(shù)靈活一些了
這里需要注意,sync.Once 中執(zhí)行的函數(shù),如果出現(xiàn)了 panic
,也是會(huì)被認(rèn)為是執(zhí)行完了了一次,之后如果再有邏輯需要進(jìn)入 sync.Once 是無法進(jìn)入并執(zhí)行函數(shù)邏輯的
一般情況下, sync.Once 用于對象資源的初始化和清理動(dòng)作,避免重復(fù)操作,可以來看一個(gè) demo:
- 主函數(shù)開辟 3 個(gè)協(xié)程,且使用 sync.WaitGroup 來管控并等待子協(xié)程退出
- 主函數(shù)開辟所有協(xié)程之后等待 2 秒,開始創(chuàng)建并獲取實(shí)例
- 協(xié)程中也在獲取實(shí)例
- 只要有一個(gè)協(xié)程獲取到進(jìn)入 Once,執(zhí)行邏輯之后,會(huì)出現(xiàn) panic
- 出現(xiàn) panic 的協(xié)程捕獲了異常,此時(shí)全局的 instance 已經(jīng)被初始化,其他協(xié)程仍然無法進(jìn)入 Once 內(nèi)的函數(shù)
type Instance struct { Name string } var instance *Instance var on sync.Once func GetInstance(num int) *Instance { defer func() { if err := recover(); err != nil { fmt.Println("num %d ,get instance and catch error ... \n", num) } }() on.Do(func() { instance = &Instance{Name: "阿兵云原生"} fmt.Printf("%d enter once ... \n", num) panic("panic....") }) return instance } func main() { var wg sync.WaitGroup for i := 0; i < 3; i++ { wg.Add(1) go func(i int) { ins := GetInstance(i) fmt.Printf("%d: ins:%+v , p=%p\n", i, ins, ins) wg.Done() }(i) } time.Sleep(time.Second * 2) ins := GetInstance(9) fmt.Printf("9: ins:%+v , p=%p\n", ins, ins) wg.Wait() }
通過打印結(jié)果可以看出,0 對應(yīng)的協(xié)程進(jìn)入了 Once,且發(fā)生了 panic,因此當(dāng)前協(xié)程獲取到的 GetInstance 函數(shù)的結(jié)果是 nil
其他的協(xié)程包括主協(xié)程調(diào)用 GetInstance 函數(shù)都能正常拿到 instance 的地址,可以看出地址是同一個(gè),全局就只初始化了一次
$ go run main.go
0 enter once ...
num %d ,get instance and catch error ...
0
0: ins:<nil> , p=0x0
1: ins:&{Name:阿兵云原生} , p=0xc000086000
2: ins:&{Name:阿兵云原生} , p=0xc000086000
9: ins:&{Name:阿兵云原生} , p=0xc000086000
到此這篇關(guān)于淺析GO并發(fā)處理選擇sync還是channel的文章就介紹到這了,更多相關(guān)go并發(fā)處理內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Golang import本地包和導(dǎo)入問題相關(guān)詳解
這篇文章主要介紹了Golang import本地包和導(dǎo)入問題相關(guān)詳解,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-02-02完美解決go Fscanf 在讀取文件時(shí)出現(xiàn)的問題
這篇文章主要介紹了完美解決go Fscanf 在讀取文件時(shí)出現(xiàn)的問題,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-03-03Go項(xiàng)目配置管理神器之viper的介紹與使用詳解
viper是一個(gè)完整的?Go應(yīng)用程序的配置解決方案,它被設(shè)計(jì)為在應(yīng)用程序中工作,并能處理所有類型的配置需求和格式,下面這篇文章主要給大家介紹了關(guān)于Go項(xiàng)目配置管理神器之viper的介紹與使用,需要的朋友可以參考下2023-02-02源碼剖析Golang中map擴(kuò)容底層的實(shí)現(xiàn)
之前的文章詳細(xì)介紹過Go切片和map的基本使用,以及切片的擴(kuò)容機(jī)制。本文針對map的擴(kuò)容,會(huì)從源碼的角度全面的剖析一下map擴(kuò)容的底層實(shí)現(xiàn),需要的可以參考一下2023-03-03golang協(xié)程關(guān)閉踩坑實(shí)戰(zhàn)記錄
協(xié)程(coroutine)是Go語言中的輕量級(jí)線程實(shí)現(xiàn),下面這篇文章主要給大家介紹了關(guān)于golang協(xié)程關(guān)閉踩坑的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-03-03golang中的string與其他格式數(shù)據(jù)的轉(zhuǎn)換方法詳解
這篇文章主要介紹了golang中的string與其他格式數(shù)據(jù)的轉(zhuǎn)換方法,文章通過代碼示例介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作有一定的幫助,需要的朋友可以參考下2023-10-10