你們好,我是煎魚。web
最近在看 Go 併發相關的內容,發現仍是有很多細節容易讓人迷迷糊糊的,一個不當心就踏入深坑裏,且指不定要在上線跑了一些數據後才能發現,那可真是太人崩潰了。微信
今天來分享幾個案例,但願你們在編碼時可以避開這幾個 「坑」。架構
案例一
演示代碼
第一個案例來自 @鳥窩 大佬在極客時間的分享,代碼以下:併發
func main() {
count := 0
wg := sync.WaitGroup{}
wg.Add(10)
for i := 0; i < 10; i++ {
go func() {
defer wg.Done()
for j := 0; j < 100000; j++ {
count++
}
}()
}
wg.Wait()
fmt.Println(count)
}
思考一下,最後輸出的 count
變量的值是多少?是否是一百萬?app
輸出結果
在上述代碼中,咱們經過 for-loop
循環起 goroutine
進行自增,並使用了 sync.WaitGroup
來保證全部的 goroutine 都執行完畢才輸出最終的結果值。編輯器
最終的輸出結果以下:微服務
// 第一次執行
638853
// 第二次執行
654473
// 第三次執行
786193
輸出的結果值不是恆定的,也就是每次輸出的都不同,且基本不會達到想象中的一百萬。oop
分析緣由
其緣由在於 count++
並非一個原子操做,在彙編上就包含了好幾個動做,以下:性能
MOVQ "".count(SB), AX
LEAQ 1(AX), CX
MOVQ CX, "".count(SB)
由於可能會同時存在多個 goroutine 同時讀取到 count
的值爲 1212,並各自自增 1,再將其寫回。學習
與此同時也會有其餘的 goroutine 可能也在其自增時讀到了值,造成了互相覆蓋的狀況,這是一種併發訪問共享數據的錯誤。
發現問題
這類競爭問題能夠經過 Go 語言所提供的的 race 檢測(Go race detector)來進行分析和發現:
$ go run -race main.go
==================
WARNING: DATA RACE
Read at 0x00c0000c6008 by goroutine 13:
main.main.func1()
/Users/eddycjy/go-application/awesomeProject/main.go:28 +0x78
Previous write at 0x00c0000c6008 by goroutine 7:
main.main.func1()
/Users/eddycjy/go-application/awesomeProject/main.go:28 +0x91
Goroutine 13 (running) created at:
main.main()
/Users/eddycjy/go-application/awesomeProject/main.go:25 +0xe4
Goroutine 7 (running) created at:
main.main()
/Users/eddycjy/go-application/awesomeProject/main.go:25 +0xe4
==================
...
489194
Found 3 data race(s)
exit status 66
編譯器會經過探測全部的內存訪問,監聽其內存地址的訪問(讀或寫)。在應用運行時就可以發現對共享變量的訪問和操做,進而發現問題並打印出相關的警告信息。
須要注意的一點是,go run -race
是運行時檢測,並非編譯時。且 race 存在明確的性能開銷,一般是正常程序的十倍,所以不要想不開在生產環境打開這個配置,很容易翻車。
案例二
演示代碼
第二個案例來自煎魚在腦子的分享,代碼以下:
func main() {
wg := sync.WaitGroup{}
wg.Add(5)
for i := 0; i < 5; i++ {
go func(i int) {
defer wg.Done()
fmt.Println(i)
}(i)
}
wg.Wait()
}
思考一下,最後輸出的結果是什麼?值都是 4 嗎?輸出是穩定有序的嗎?
輸出結果
在上述代碼中,咱們經過 for-loop
循環起了多個 goroutine
,並將變量 i
做爲形參傳遞給了 goroutine
,最後在 goroutine
內輸出了變量 i
。
最終的輸出結果以下:
// 第一次輸出
0
1
2
4
3
// 第二次輸出
4
0
1
2
3
顯然,從結果上來看,輸出的值都是無序且不穩定的,值更不是 4。這究竟是爲何?
分析緣由
其緣由在於,即便全部的 goroutine
都建立完了,但 goroutine
不必定已經開始運行了。
也就是等到 goroutine
真正去執行輸出時,變量 i
(值拷貝)可能已經不是建立時的值了。
其整個程序扭轉實質上分爲了多個階段,也就是各自運行的時間線並不一樣,能夠其拆分爲:
-
先建立:
for-loop
循環建立goroutine
。 -
再調度:協程
goroutine
開始調度執行。 -
才執行:開始執行
goroutine
內的輸出。
同時 goroutine
的調度存在必定的隨機性(建議瞭解一下 GMP 模型),那麼其輸出的結果就勢必是無序且不穩定的。
發現問題
這時候你可能會想,那前面提到的 go run -race
能不能發現這個問題呢。以下:
$ go run -race main.go
0
1
2
3
4
沒有出現警告,顯然是不能的,由於其本質上並非併發訪問共享數據的錯誤,且會致使程序變成了串行,從而矇蔽了你的雙眼。
案例三
演示代碼
第三個案例來自煎魚在夢裏的分享,代碼以下:
func main() {
wg := sync.WaitGroup{}
wg.Add(5)
for i := 0; i < 5; i++ {
go func() {
defer wg.Done()
fmt.Println(i)
}()
}
wg.Wait()
}
思考一下,最後輸出的結果是什麼?值都是 4 嗎?會像案例二同樣亂竄嗎?
輸出結果
在上述代碼中,與案例二大致沒有區別,主要是變量 i
沒有做爲形參傳入。
最終的輸出結果以下:
// 第一次輸出
5
5
5
5
5
初步從輸出的結果上來看都是 5,這時候就會有人迷糊了,爲何不是 4 呢?
很多人會因不是 4 而陷入了迷惑,但千萬不要被一兩次的輸出迷惑了心智,認爲鐵定就是 5 了。能夠再動手多輸出幾回,以下:
// 多輸出幾回
5
3
5
5
5
最終會發現...輸出結果存在隨機性,輸出結果並非 100% 都是 5,更不用提 4 了。這究竟是爲何呢?
分析緣由
其緣由與案例二其實很是接近,理論上理解了案例二也就能解決案例三。
其本質仍是建立 goroutine
與真正執行 fmt.Println
並不一樣步。所以頗有可能在你執行 fmt.Println
時,循環 for-loop
已經運行完畢,所以變量 i
的值最終變成了 5。
那麼相反,其也有可能沒運行完,存在隨機性。寫個 test case 就能發現明顯的不一樣。
總結
在本文中,我分享了幾個近期看到次數最頻繁的一些併發上的小 「坑」,但願對你有所幫助。
同時你也能夠回想一下,在你編寫 Go 併發程序有沒有也遇到過什麼問題?
歡迎你們一塊兒討論交流。
分享 Go 語言、微服務架構和奇怪的系統設計
👆 長按關注煎魚,在知識的海洋裏遨遊
學習資料分享,關注公衆號回覆指令:
-
回覆【000】,下載 LeetCode 題解大全。 -
回覆【001】,下載 Go 進階圖書 Mastering Go。
本文分享自微信公衆號 - 腦子進煎魚了(eddycjy)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。