如有任何問題或建議,歡迎及時交流和碰撞。個人公衆號是 【腦子進煎魚了】,GitHub 地址: https://github.com/eddycjy。
你們好,我是煎魚。git
最近在看 Go 併發相關的內容,發現仍是有很多細節容易讓人迷迷糊糊的,一個不當心就踏入深坑裏,且指不定要在上線跑了一些數據後才能發現,那可真是太人崩潰了。github
今天來分享幾個案例,但願你們在編碼時可以避開這幾個 「坑」。golang
第一個案例來自 @鳥窩 大佬在極客時間的分享,代碼以下:架構
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
變量的值是多少?是否是一百萬?併發
在上述代碼中,咱們經過 for-loop
循環起 goroutine
進行自增,並使用了 sync.WaitGroup
來保證全部的 goroutine 都執行完畢才輸出最終的結果值。app
最終的輸出結果以下:微服務
// 第一次執行 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 併發程序有沒有也遇到過什麼問題?
分享 Go 語言、微服務架構和奇怪的系統設計,歡迎你們關注個人公衆號和我進行交流和溝通。
最好的關係是互相成就,各位的點贊就是煎魚創做的最大動力,感謝支持。