Golang 併發,有快取通道,通道同步案例演示

nlh1996發表於2019-05-13

學習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
複製程式碼

發現即使只有單一的協程與主執行緒共享變數,也是會發生問題。結論:協程間儘量不要共享變數,很難保證不出問題。說這麼多隻是體現通道的作用與優點。

以上全部內容只是個人的一點摸索,不代表完全正確。

相關文章