併發本身並不複雜,但是因為有了資源競爭的問題,就使得我們開發出好的併發程式變得複雜起來,因為會引起很多莫名其妙的問題。
package mainimport (
"fmt"
"runtime"
"sync"
)var (
count int32
wg sync.waitgroup
)func main()
func inccount()
}
這是乙個資源競爭的例子。我們可以多執行幾次這個程式,會發現結果可能是 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/***/code/go/src/flysnow.org/hello/main.go:25 +0x76
previous write at 0x0000011a5118 by goroutine 6:
main.inccount()
/users/***/code/go/src/flysnow.org/hello/main.go:28 +0x9a
goroutine 7 (running) created at:
main.main()
/users/***/code/go/src/flysnow.org/hello/main.go:17 +0x77
goroutine 6 (finished) created at:
main.main()
/users/***/code/go/src/flysnow.org/hello/main.go:16 +0x5f
***************===
4found 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 mainimport (
"fmt"
"runtime"
"sync"
"sync/atomic"
)var (
count int32
wg sync.waitgroup
)func main()
func inccount()
}
留意這裡atomic.loadint32
和atomic.storeint32
兩個函式:乙個讀取int32型別變數的值,乙個是修改int32型別變數的值。這兩個都是原子性的操作,go已經幫助我們在底層使用加鎖機制,保證了共享資源的同步和安全,所以我們可以得到正確的結果。這時候我們再使用資源競爭檢測工具go build -race
檢查,也不會提示有問題了。
atomic包裡還有很多原子化的函式可以保證併發下資源同步訪問修改的問題。比如函式atomic.addint32
可以直接對乙個int32型別的變數進行修改,在原值的基礎上再增加多少的功能,也是原子性的。這裡不再舉例,大家自己可以試試。
atomic雖然可以解決資源競爭問題,但是比較都是比較簡單的,支援的資料型別也有限,所以go語言還提供了乙個sync包。這個sync包裡提供了一種互斥型的鎖,可以讓我們自己靈活地控制那些**,同時只能有乙個goroutine訪問,被sync互斥鎖控制的這段**範圍,被稱之為臨界區。臨界區的**,同一時間,只能
又乙個goroutine訪問。剛剛那個例子,我們還可以這麼改造。
package mainimport (
"fmt"
"runtime"
"sync"
)var (
count int32
wg sync.waitgroup
mutex sync.mutex
)func main()
func inccount()
}
例項中,新宣告了乙個互斥鎖mutex sync.mutex
。這個互斥鎖有兩個方法,乙個是mutex.lock()
,乙個是mutex.unlock()
。
這兩個之間的區域就是臨界區,臨界區的**是安全的。
示例中我們先呼叫mutex.lock()
對有競爭資源的**加鎖,這樣當乙個goroutine進入這個區域的時候,其他goroutine就進不來了,只能等待,一直到呼叫mutex.unlock()
釋放這個鎖為止。
這種方式比較靈活,可以讓**編寫者任意定義需要保護的**範圍,也就是臨界區。除了原子函式和互斥鎖,go還為我們提供了更容易在多個goroutine同步的功能,這就是通道chan,我們會在下次繼續講解。
Go語言之併發程式設計(四)
同步 go 程式可以使用通道進行多個 goroutine 間的資料交換,但這僅僅是資料同步中的一種方法。通道內部的實現依然使用了各種鎖,因此優雅 的代價是效能。在某些輕量級的場合,原子訪問 atomic包 互斥鎖 sync.mutex 以及等待組 sync.waitgroup 能最大程度滿足需求。當...
Go語言之併發程式設計(三)
telnet回音伺服器 telnet協議是tcp ip協議族中的一種。它允許使用者 telnet客戶端 通過乙個協商過程與乙個遠端裝置進行通訊。本例將使用一部分telnet協議與伺服器進行通訊。伺服器的網路庫為了完整展示自己的 實現了完整的收發過程,一般比較傾向於使用傳送任意封包返回原資料的邏輯。這...
Go語言之併發程式設計(一)
輕量級執行緒 goroutine 雖然,執行緒池為邏輯編寫者提供了執行緒分配的抽象機制。但是,如果面對隨時隨地可能發生的併發和執行緒處理需求,執行緒池就不是非常直觀和方便了。能否有一種機制 使用者分配足夠多的任務,系統能自動幫助使用者把任務分配到cpu上,讓這些任務盡量併發運作。這種機制在go語言中...