學習Golang差不多半年了,go中的併發,通道,通道同步單個來講都不陌生,但是結合在一起運用的時候就有些懵逼,同時也不知道為何要這麼做。我想這是初學者都會遇到的困惑,在這裡講下自己的理解。
為什麼用通道而不是共享變數
看一段程式碼
func main() {
var a int
for i := 0; i < 10; i++ {
go func() {
for i := 0; i < 100; i++ {
a++
}
}()
}
time.Sleep(1 * time.Second)
fmt.Print(a)
}
// 執行結果
PS C:\Users\mayn\go\src\test_5_5> go run .\main.go
1000
複製程式碼
從執行結果來看,主執行緒是可以跟協程共享變數的,同時10個協程分別自加100次,得到1000的結果與預期結果一樣
現在增加每個協程的運算量,再看一下執行結果
func main() {
var a int
for i := 0; i < 10; i++ {
go func() {
for i := 0; i < 100000; i++ {
a++
}
}()
}
time.Sleep(1 * time.Second)
fmt.Print(a)
}
// 輸出結果
PS C:\Users\mayn\go\src\test_5_5> go run .\main.go
213897
PS C:\Users\mayn\go\src\test_5_5> go run .\main.go
206400
PS C:\Users\mayn\go\src\test_5_5> go run .\main.go
211926
複製程式碼
可以看到每個協程由100的自加變為100000的自加,此時輸出結果每次都不同並且與1000000的預期結果相差很大,個人沒有深入研究只是簡單推測由於併發的非同步特性,同一時間有多個協程執行了自增,實際cpu只計算了一次,這種誤差會隨著併發協程的數量和各自計算量的增多而變大。(後來有人補充cpu核數限制為1核就不會發生這種並行的情況)
使用有快取的通道得出正確結果
func main() {
var ch = make(chan int, 10)
for i := 0; i < 10; i++ {
go func() {
var a int
for i := 0; i < 100000; i++ {
a++
}
ch <- a
}()
}
var sum int
func() {
for i := 0; i < 10; i++ {
sum += <- ch
}
}()
fmt.Print(sum)
}
// 輸出結果
PS C:\Users\mayn\go\src\test_5_5> go run .\main.go
1000000
複製程式碼
大致思路還是開啟10個協程,同時將原來定義在主執行緒中的變數a定義到每個協程中,在主執行緒中定義有10個緩衝的通道。這時每個協程各自處理自己的運算結果互不干擾,只在最後將各自運算結果寫入到通道中。主執行緒再遍歷通道進行讀操作,只有當協程中有資料被寫入時才能讀取到資料並且彙總結果。由於讀操作是在主執行緒中會發生阻塞,所以此時可以去掉睡眠,程式依然能正確執行,這就是通道同步。
如果通道讀操作也開一個協程來處理會怎麼樣
func main() {
var ch = make(chan int, 10)
for i := 0; i < 10; i++ {
go func() {
var a int
for i := 0; i < 100000; i++ {
a++
}
ch <- a
}()
}
var sum int
go func() {
for i := 0; i < 10; i++ {
sum += <- ch
}
}()
fmt.Print(sum)
}
// 輸出結果
PS C:\Users\mayn\go\src\test_5_5> go run .\main.go
0
複製程式碼
很明顯如果讀操作也開協程,此時主執行緒不會發生阻塞,主執行緒不等協程結束直接結束了,想要得到正確結果,主要主執行緒等待就行了。這樣做的優點就是讀操作也是併發的,不需要同步等待。
協程與主執行緒共享變數
還是這段程式碼,加上時間等待。
func main() {
var ch = make(chan int, 10)
for i := 0; i < 10; i++ {
go func() {
var a int
for i := 0; i < 100000; i++ {
a++
}
ch <- a
}()
}
var sum int
go func() {
for i := 0; i < 10; i++ {
sum += <- ch
}
}()
time.Sleep(1 * time.Second)
fmt.Print(sum)
}
// 輸出結果
PS C:\Users\mayn\go\src\test_5_5> go run .\main.go
1000000
複製程式碼
細心觀察,可以發現併發通道讀操作的結果使用了主執行緒的變數sum,程式按預期正確執行。這就說明了協程是可以跟主執行緒共享變數的,只是使用的前提是這個變數只被一個協程使用,如果被多個協程使用就可能出現文章開頭出現的問題。
假如主執行緒與協程同時操作一個變數
func main() {
var a int
go func() {
for i := 0; i < 1000000; i++ {
a++
}
}()
for i := 0; i < 1000000; i++ {
a++
}
time.Sleep(1 * time.Second)
fmt.Print(a)
}
// 輸出
PS C:\Users\mayn\go\src\test_5_5> go run .\main.go
1079312
PS C:\Users\mayn\go\src\test_5_5> go run .\main.go
1003960
PS C:\Users\mayn\go\src\test_5_5> go run .\main.go
1021828
複製程式碼
發現即使只有單一的協程與主執行緒共享變數,也是會發生問題。結論:協程間儘量不要共享變數,很難保證不出問題。說這麼多隻是體現通道的作用與優點。
以上全部內容只是個人的一點摸索,不代表完全正確。