用一個小例子談談Golang 中的Race Condition

Goroutine 是Go 最重要的特性之一,它可以讓開發者輕易做到併發(concurrency),而且他的非常輕量,所以一次開一大堆goroutine 也不會有什麼問題。

但如果在使用goroutine 時沒有考慮到race condition,那可能就會導致不正確的結果,這篇文章會用一個簡單的小例子,談談在什麼情況可能會遇到race condition,以及如何發現、解決它。

用一個小例子談談Golang 中的Race Condition

Race Condition(競爭危害)

譬如說有兩個正在進行中的goroutine分別要對某變數a做a = a * 2還有a = a + 1,這兩個goroutine不同的順序有可能導致最後a有不同的值,這就是race condition,為了防止race condition要使用一些特別的方式讓他們有確定的順序,以免導致奇怪的bug

來看看這次要講解的例子,分成三個步驟

  1. 先把a 的初始值設為 0
  2. 開三個goroutine 共做了三次 a++
  3. 最後用channel 等待三個goroutine 完成

沒意外的話最後應該會得到a = 3,結果也確實如此

那如果把次數改成一萬次呢?

理論上要得到a = 10000,實際跑了卻會得到a = 9903之類的結果(如果你有多核CPU的話),但我們確實開了一萬個goroutine也做了一萬次a++,為什麼結果會不對呢,因為在a++的過程中發生了race condition

為什麼a++ 會發生race condition

當你寫了a++時電腦實際上做了三件事:

  1. CPU 把a 的值取出來
  2. 把剛剛取得的值加 1
  3. 把運算的結果存回變數 a

但萬一你有多核CPU就有可能會這樣:

用一個小例子談談Golang 中的Race Condition

兩個CPU 同時去拿變數a 的值,各自加1 後存回,導致a 只被加了一次,因此結果(9903)會小於正確的10000

解法:互斥鎖

這裡會發生race condition最根本的原因是「兩個goroutine可能會同時存取變數a」,如果能限制同時只能有一個 goroutine做a++,那就能解決這個問題,為了達到這個目的我們要使用sync.Mutex

Mutex是互斥鎖的意思,同時最多隻能有一個人拿到這個鎖,其他人想要鎖的話就會被block住直到前一個人用完,所以就可以確保只有一個goroutine正在進行a++,這樣就可以得到正確的結果10000

如何發現race condition

在這個例子中race condition發生在a++,但如果對電腦底層不夠熟悉就有可能沒辦法發現問題,還好Golang有個很強大的工具叫Data Race Detector

在跑的時候加一個-race他就可以幫你偵測哪邊可能會產生race condiction,大家也可以自己下載原碼下來跑

$ go run -race add_few_times/main.go 
==================
WARNING: DATA RACE
Read at 0x00c4200a4008 by goroutine 7:
main.main.func1()
.../ add_few_times/main.go:12 +0x38

Previous write at 0x00c4200a4008 by goroutine 6:
main.main.func1()
.../add_few_times/main.go:12 +0x4e
Goroutine 7 (running) created at:
main.main()
.../add_few_times/main.go:11 +0xc1
Goroutine 6 (running) created at:
main.main()
.../add_few_times/main.go:11 +0xc1
==================
Found 1 data race(s )
exit status 66

Race Detector說在第11行(go func(){…}())產生了兩個goroutine(G6跟G7),G7在第十二行(a++)讀取了變數a之後,G6緊接著寫入了變數a,所以G7會讀到舊資料,這時候就有可能會產生race condition

透過Race Detector 幾乎可以找到所有的race condition,大部分時候也都只要加個鎖就可以解決

鎖的缺點

性能

上面的例子用mutex 來防止多個goroutine 同時存取同一個變數,因為總共有一萬個goroutine,當你有其中一個正在存取a 時其他9999 個都在等他,他們之間完全沒有並行( parallelism),不如用個迴圈把它從0 加到10000 可能還更快

所以在使用鎖時一定要非常小心,只在必要的時候使用,否則效能將會大打折扣

忘記解鎖

有時候上鎖解鎖不像上面a++這麼簡單,中間可能有很多個鎖還有各種條件判斷、網路請求等等,當程式變複雜一不小就有可能忘記或是太晚解鎖,造成整個程式非常慢甚至完全卡住,產生死鎖問題

總結

這次用很簡單的例子談談在Golang 中什麼時候會遇到race condition 以及如何解決,因為要開一個goroutine 太簡單了,所以有時候會不小心忘記考慮race condition,還好Go 有提供Race Detector 不用自己慢慢找XDD


分享到:


相關文章: