在同步執行中,程序是一個接一個地執行,而運行時中除了當前正在執行的操作之外沒有發生任何其他事情。與之對比的是並發執行,並發執行存在著許多不確定性,這種不確定性意味著共享記憶體可以由不同的工作線程以意想不到的方式操作,一個工作單元可能被搶占,從而產生大量潛在的執行順序問題。
由於並發性是不確定的,因此它可能會導致極其危險的bug。代碼可能看起來很好並通過了測試,但是在高並發性或某些代碼路徑執行中,它會導致顯式的提升或微妙的破壞行為。
競爭條件是兩個線程同時訪問記憶體,其中一個線程的工作內容是將資料寫入記憶體。競爭條件的出現是由於對共享記憶體的不同步訪問。
維基百科對 Race Condition 的定義
A race condition or race hazard is the behavior of an electronics, software, or other system where the output is dependent on the sequence or timing of other uncontrollable events.
package main
import (
"fmt"
"runtime"
"sync"
)
var (
count int32
wg sync.WaitGroup
)
func main() {
wg.Add(2)
go incCount()
go incCount()
wg.Wait()
fmt.Println(count)
}
func incCount() {
defer wg.Done()
for i := 0; i < 2; i++ {
value := count
runtime.Gosched()
value++
count = value
}
}
這是一個競爭條件的例子,當我們並發執行上面的程式碼時,會發現執行結果可能是2、3、或者4。因此共享資源 count 沒有受到任何的同步保護,因此兩個goroutine都會對其進行讀寫操作,並且相互覆蓋結果。
我們試著描述其中一個可能性,假如現在有兩個goroutine分別為g1和g2:
在這個例子裡,我們看到兩個Gorountine都對count變數+1,結果變數只有+1,而不是我們預期的+2。
所以我們對於同一個資源的讀寫必須是原子化的,也就是說同一時間只能有一個goroutine對共享資源進行讀寫操作。
Go語言提供了一個檢查器,讓我們可以利用它去進行程式碼是否存在競爭條件的問題,使用的方法也很簡單,只要加上--race的標志:
由於它在底層使用了ThreadSanitizer庫,你應該預期運行測試時速度會降低2-20倍,而且記憶體消耗會增加5-10倍,運行的其他需求是支持CGO和64位操作系統。
當你使用--race進行競爭條件檢測試,編釋器會創建一個附帶記錄程式運行時會訪問共享變量的工具,並且會記錄每一個讀寫共享變量的gorountine的身份訊息。此外,所有的同步事件,比如 go 語句,channel操 作,以及對(*sync.Mutex).Lock,(*sync.WaitGroup).Wait等等的調用。都會被記錄下來,並且一一的檢查是否在運行時發生了競爭條件。