Go語言之並發資源競爭
並發本身並不復雜,但是因為有了資源競爭的問題,就使得我們開發出好的並發程序變得復雜起來,因為會引起很多莫名其妙的問題。
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。
g1讀取到count為 0 。
然後g1暫停了,切換到g2運行,g2讀取到count也為 0 。
g2暫停,切換到g1,g1對count+1,count變為 1 。
g1暫停,切換到g2,g2剛剛已經獲取到值 0 ,對其+1,最後賦值給count還是 1 。
有沒有註意到,剛剛g1對count+1的結果被g2給覆蓋了,兩個goroutine都+1還是 1 。
不再繼續演示下去了,到這裏結果已經錯了,兩個goroutine相互覆蓋結果。我們這裏的runtime.Gosched()
是讓當前goroutine暫停的意思。退回執行隊列,讓其他等待的goroutine運行,目的是讓我們演示資源競爭的結果更明顯。註意,這裏還會牽涉到CPU問題,多核會並行,那麽資源競爭的效果更明顯。
所以我們對於同一個資源的讀寫必須是原子化的,也就是說,同一時間只能有一個goroutine對共享資源進行讀寫操作。
共享資源競爭的問題,非常復雜,並且難以察覺,好在Go提供了一個工具來幫助我們檢查,這個就是go build -race
命令。我們在當前項目目錄下執行這個命令,生成一個可以執行文件,然後再運行這個可執行文件,就可以看到打印出的檢測信息。
go build -race
多加了一個-race
標誌,這樣生成的可執行程序就自帶了檢測資源競爭的功能。下面我們運行,也是在終端運行。
./hello
我這裏示例生成的可執行文件名是hello
,所以是這麽運行的。這時候,我們看終端輸出的檢測結果。
hello ./hello ================== WARNING: DATA RACE Read at 0x0000011a5118 by goroutine 7: main.incCount() /Users/xxx/code/go/src/flysnow.org/hello/main.go:25 +0x76 Previous write at 0x0000011a5118 by goroutine 6: main.incCount() /Users/xxx/code/go/src/flysnow.org/hello/main.go:28 +0x9a Goroutine 7 (running) created at: main.main() /Users/xxx/code/go/src/flysnow.org/hello/main.go:17 +0x77 Goroutine 6 (finished) created at: main.main() /Users/xxx/code/go/src/flysnow.org/hello/main.go:16 +0x5f ================== 4 Found 1 data race(s)
看,找到一個資源競爭,連在那一行代碼出了問題,都標示出來了。goroutine 7在代碼 25 行讀取共享資源value := count
,而這時goroutine 6正在代碼 28 行修改共享資源count = value
,而這兩個goroutine都是從main函數啟動的,在 16、17 行,通過go關鍵字。
既然我們已經知道共享資源競爭的問題,是因為同時有兩個或者多個goroutine對其進行了讀寫,那麽我們只要保證,同時只有一個goroutine讀寫不就可以了。現在我們就看下傳統解決資源競爭的辦法——對資源加鎖。
Go語言提供了atomic包和sync包裏的一些函數對共享資源同步枷鎖,我們先看下atomic包。
package main import ( "fmt" "runtime" "sync" "sync/atomic" ) 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 := atomic.LoadInt32(&count) runtime.Gosched() value++ atomic.StoreInt32(&count,value) } }
留意這裏atomic.LoadInt32
和atomic.StoreInt32
兩個函數:一個讀取int32類型變量的值,一個是修改int32類型變量的值。這兩個都是原子性的操作,Go已經幫助我們在底層使用加鎖機制,保證了共享資源的同步和安全,所以我們可以得到正確的結果。這時候我們再使用資源競爭檢測工具go build -race
檢查,也不會提示有問題了。
atomic包裏還有很多原子化的函數可以保證並發下資源同步訪問修改的問題。比如函數atomic.AddInt32
可以直接對一個int32類型的變量進行修改,在原值的基礎上再增加多少的功能,也是原子性的。這裏不再舉例,大家自己可以試試。
atomic雖然可以解決資源競爭問題,但是比較都是比較簡單的,支持的數據類型也有限,所以Go語言還提供了一個sync包。這個sync包裏提供了一種互斥型的鎖,可以讓我們自己靈活地控制那些代碼,同時只能有一個goroutine訪問,被sync互斥鎖控制的這段代碼範圍,被稱之為臨界區。臨界區的代碼,同一時間,只能又一個goroutine訪問。剛剛那個例子,我們還可以這麽改造。
package main import ( "fmt" "runtime" "sync" ) var ( count int32 wg sync.WaitGroup mutex sync.Mutex ) 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++ { mutex.Lock() value := count runtime.Gosched() value++ count = value mutex.Unlock() } }
實例中,新聲明了一個互斥鎖mutex sync.Mutex
。這個互斥鎖有兩個方法,一個是mutex.Lock()
,一個是mutex.Unlock()
。這兩個之間的區域就是臨界區,臨界區的代碼是安全的。
示例中我們先調用mutex.Lock()
對有競爭資源的代碼加鎖,這樣當一個goroutine進入這個區域的時候,其他goroutine就進不來了,只能等待,一直到調用mutex.Unlock()
釋放這個鎖為止。
這種方式比較靈活,可以讓代碼編寫者任意定義需要保護的代碼範圍,也就是臨界區。除了原子函數和互斥鎖,Go還為我們提供了更容易在多個goroutine同步的功能,這就是通道chan,我們會在下次繼續講解。
本文出自 “baby神” 博客,請務必保留此出處http://babyshen.blog.51cto.com/8405584/1933211
Go語言之並發資源競爭