新聞中心
有并發(fā),就有資源競爭,如果兩個或者多個 goroutine 在沒有相互同步的情況下,訪問某個共享的資源,比如同時對該資源進行讀寫時,就會處于相互競爭的狀態(tài),這就是并發(fā)中的資源競爭。

創(chuàng)新互聯(lián)公司專業(yè)為企業(yè)提供容城網(wǎng)站建設、容城做網(wǎng)站、容城網(wǎng)站設計、容城網(wǎng)站制作等企業(yè)網(wǎng)站建設、網(wǎng)頁設計與制作、容城企業(yè)網(wǎng)站模板建站服務,十余年容城做網(wǎng)站經(jīng)驗,不只是建網(wǎng)站,更提供有價值的思路和整體網(wǎng)絡服務。
并發(fā)本身并不復雜,但是因為有了資源競爭的問題,就使得我們開發(fā)出好的并發(fā)程序變得復雜起來,因為會引起很多莫名其妙的問題。
下面的代碼中就會出現(xiàn)競爭狀態(tài):
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
}
}這是一個資源競爭的例子,大家可以將程序多運行幾次,會發(fā)現(xiàn)結果可能是 2,也可以是 3,還可能是 4。這是因為 count 變量沒有任何同步保護,所以兩個 goroutine 都會對其進行讀寫,會導致對已經(jīng)計算好的結果被覆蓋,以至于產(chǎn)生錯誤結果。
代碼中的 runtime.Gosched() 是讓當前 goroutine 暫停的意思,退回執(zhí)行隊列,讓其他等待的 goroutine 運行,目的是為了使資源競爭的結果更明顯。
下面我們來分析一下程序的運行過程,將兩個 goroutine 分別假設為 g1 和 g2:
- g1 讀取到 count 的值為 0;
- 然后 g1 暫停了,切換到 g2 運行,g2 讀取到 count 的值也為 0;
- g2 暫停,切換到 g1,g1 對 count+1,count 的值變?yōu)?1;
- g1 暫停,切換到 g2,g2 剛剛已經(jīng)獲取到值 0,對其 +1,最后賦值給 count,其結果還是 1;
- 可以看出 g1 對 count+1 的結果被 g2 給覆蓋了,兩個 goroutine 都 +1 而結果還是 1。
通過上面的分析可以看出,之所以出現(xiàn)上面的問題,是因為兩個 goroutine 相互覆蓋結果。
所以我們對于同一個資源的讀寫必須是原子化的,也就是說,同一時間只能允許有一個 goroutine 對共享資源進行讀寫操作。
共享資源競爭的問題,非常復雜,并且難以察覺,好在 Go 為我們提供了一個工具幫助我們檢查,這個就是go build -race 命令。在項目目錄下執(zhí)行這個命令,生成一個可以執(zhí)行文件,然后再運行這個可執(zhí)行文件,就可以看到打印出的檢測信息。
在go build命令中多加了一個-race 標志,這樣生成的可執(zhí)行程序就自帶了檢測資源競爭的功能,運行生成的可執(zhí)行文件,效果如下所示:
==================
WARNING: DATA RACE
Read at 0x000000619cbc by goroutine 8:
main.incCount()
D:/code/src/main.go:25 +0x80
Previous write at 0x000000619cbc by goroutine 7:
main.incCount()
D:/code/src/main.go:28 +0x9f
Goroutine 8 (running) created at:
main.main()
D:/code/src/main.go:17 +0x7e
Goroutine 7 (finished) created at:
main.main()
D:/code/src/main.go:16 +0x66
==================
4
Found 1 data race(s)
通過運行結果可以看出 goroutine 8 在代碼 25 行讀取共享資源 value := count,而這時 goroutine 7 在代碼 28 行修改共享資源 count = value,而這兩個 goroutine 都是從 main 函數(shù)的 16、17 行通過 go 關鍵字啟動的。
鎖住共享資源
Go語言提供了傳統(tǒng)的同步 goroutine 的機制,就是對共享資源加鎖。atomic 和 sync 包里的一些函數(shù)就可以對共享的資源進行加鎖操作。
原子函數(shù)
原子函數(shù)能夠以很底層的加鎖機制來同步訪問整型變量和指針,示例代碼如下所示:
package main
import (
"fmt"
"runtime"
"sync"
"sync/atomic"
)
var (
counter int64
wg sync.WaitGroup
)
func main() {
wg.Add(2)
go incCounter(1)
go incCounter(2)
wg.Wait() //等待goroutine結束
fmt.Println(counter)
}
func incCounter(id int) {
defer wg.Done()
for count := 0; count < 2; count++ {
atomic.AddInt64(&counter, 1) //安全的對counter加1
runtime.Gosched()
}
}上述代碼中使用了 atmoic 包的 AddInt64 函數(shù),這個函數(shù)會同步整型值的加法,方法是強制同一時刻只能有一個 gorountie 運行并完成這個加法操作。當 goroutine 試圖去調(diào)用任何原子函數(shù)時,這些 goroutine 都會自動根據(jù)所引用的變量做同步處理。
另外兩個有用的原子函數(shù)是 LoadInt64 和 StoreInt64。這兩個函數(shù)提供了一種安全地讀和寫一個整型值的方式。下面是代碼就使用了 LoadInt64 和 StoreInt64 函數(shù)來創(chuàng)建一個同步標志,這個標志可以向程序里多個 goroutine 通知某個特殊狀態(tài)。
package main
import (
"fmt"
"sync"
"sync/atomic"
"time"
)
var (
shutdown int64
wg sync.WaitGroup
)
func main() {
wg.Add(2)
go doWork("A")
go doWork("B")
time.Sleep(1 * time.Second)
fmt.Println("Shutdown Now")
atomic.StoreInt64(&shutdown, 1)
wg.Wait()
}
func doWork(name string) {
defer wg.Done()
for {
fmt.Printf("Doing %s Work\n", name)
time.Sleep(250 * time.Millisecond)
if atomic.LoadInt64(&shutdown) == 1 {
fmt.Printf("Shutting %s Down\n", name)
break
}
}
} 上面代碼中 main 函數(shù)使用 StoreInt64 函數(shù)來安全地修改 shutdown 變量的值。如果哪個 doWork goroutine 試圖在 main 函數(shù)調(diào)用 StoreInt64 的同時調(diào)用 LoadInt64 函數(shù),那么原子函數(shù)會將這些調(diào)用互相同步,保證這些操作都是安全的,不會進入競爭狀態(tài)。
互斥鎖
另一種同步訪問共享資源的方式是使用互斥鎖,互斥鎖這個名字來自互斥的概念?;コ怄i用于在代碼上創(chuàng)建一個臨界區(qū),保證同一時間只有一個 goroutine 可以執(zhí)行這個臨界代碼。
示例代碼如下所示:
package main
import (
"fmt"
"runtime"
"sync"
)
var (
counter int64
wg sync.WaitGroup
mutex sync.Mutex
)
func main() {
wg.Add(2)
go incCounter(1)
go incCounter(2)
wg.Wait()
fmt.Println(counter)
}
func incCounter(id int) {
defer wg.Done()
for count := 0; count < 2; count++ {
//同一時刻只允許一個goroutine進入這個臨界區(qū)
mutex.Lock()
{
value := counter
runtime.Gosched()
value++
counter = value
}
mutex.Unlock() //釋放鎖,允許其他正在等待的goroutine進入臨界區(qū)
}
}同一時刻只有一個 goroutine 可以進入臨界區(qū)。之后直到調(diào)用 Unlock 函數(shù)之后,其他 goroutine 才能進去臨界區(qū)。當調(diào)用 runtime.Gosched 函數(shù)強制將當前 goroutine 退出當前線程后,調(diào)度器會再次分配這個 goroutine 繼續(xù)運行。
網(wǎng)站欄目:創(chuàng)新互聯(lián)GO教程:Go語言競爭狀態(tài)簡述
本文網(wǎng)址:http://www.dlmjj.cn/article/cdejoei.html


咨詢
建站咨詢
