Go語言的特色不得不提的就是併發機制,在C語言中編寫非常繁瑣複雜的併發程序在Go語言中可以非常便捷。
這幾天寫併發測試腳本的時候,結合代碼和其他大牛的文章學習了一下。把自己的理解寫下來。如有錯誤,請指正。
一、併發與並行
Go中併發程序主要通過goroutine和channel來實現。
這裏我想先解釋一下的是“併發”一詞,一開始把併發當做了並行,一直覺得代碼有問題,其實這兩者並不是一回事。
併發就是:兩個隊列,同時依次去訪問一個資源。而並行:兩個隊列,分別依次訪問兩個資源。
簡單來說,併發,就像一個人(cpu)喂2個孩子(程序),輪換着每人喂一口,表面上兩個孩子都在吃飯。並行,就是2個人喂2個孩子,兩個孩子也同時在吃飯。
代碼示例
以前我們調用多個線程分別打印輸出有序的數字時,系統的線程會搶佔式地輸出, 表現出來的是亂序地輸出。而多個goroutine併發執行結果是輸出一串有序的數字接着一串有序的數字。示例代碼:
var quit chan int = make(chan int)
func loop() {
for i := 0; i < 10; i++ {
fmt.Printf("%d ", i)
}
quit <- 0
}
func main() {
// 開兩個goroutine跑函數loop, loop函數負責打印10個數
go loop()
go loop()
//保證goroutine都執行完,主線程才結束
for i := 0; i < 2; i++ {
<- quit
}
}
輸出結果:
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
但我們以前用線程實現的結果是亂序的,比如這樣的:
0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9
二、goroutine是如何執行的
實際上,默認Go所有的goroutines只能在一個線程裏跑,而一個goroutine並不相當於一個線程,goroutine的出現正是爲了替代原來的線程概念成爲最小的調度單位。(一旦運行goroutine時,先去當前線程查找,如果線程阻塞了,則被分配到空閒的線程,若無空閒線程,就新建一個線程。注意的是,當goroutine執行完畢後,線程不會被回收,而是成爲了空閒的線程。)
如果當前goroutine不發生阻塞,它是不會讓出CPU給其他goroutine的, 在上面的代碼實例中,輸出會是一個一個goroutine進行,而如果使用sleep函數,則阻塞掉了當前goroutine, 當前goroutine主動讓其他goroutine執行, 所以形成了併發。
代碼實例
使用goroutine想要達到真正的並行的效果也是可以的,解決方案有兩個:
1、允許Go使用多核(runtime.GOMAXPROCS)
var quit chan int = make(chan int)
func loop() {
for i := 0; i < 100; i++ { //爲了觀察,跑多些
fmt.Printf("%d ", i)
}
quit <- 0
}
func main() {
runtime.GOMAXPROCS(2) // 最多使用2個核
go loop()
go loop()
for i := 0; i < 2; i++ {
<- quit
}
}
2、手動顯式調動(runtime.Gosched)
func loop() {
for i := 0; i < 10; i++ {
runtime.Gosched() // 顯式地讓出CPU時間給其他goroutine
fmt.Printf("%d ", i)
}
quit <- 0
}
func main() {
go loop()
go loop()
for i := 0; i < 2; i++ {
<- quit
}
}
執行結果:
0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9
第二種主動讓出CPU時間的方式仍然是在單核裏跑。但手工地切換goroutine導致了看上去的“並行”。
三、runtime
runtime調度器是個很神奇的東西,但是我但願它不存在,我希望顯式調度能更爲自然些,多核處理默認開啓。
關於runtime包幾個函數:
Gosched() //讓出cpu
NumCPU()//返回當前系統的CPU核數量
GOMAXPROCS() //設置最大的可同時使用的CPU核數
Goexit() //退出當前goroutine(但是defer語句會照常執行)
四、總結
默認所有goroutine會在一個原生線程裏跑,也就是默認只使用一個CPU核。如果當前goroutine不發生阻塞,它是不會讓出CPU時間給其他同線程的goroutines的,這是Go運行時對goroutine的調度,我們也可以使用runtime包來手工調度。
若我們開啓多核,當一個goroutine發生阻塞,Go會自動地把與該goroutine處於同一系統線程的其他goroutines轉移到另一個系統線程上去,以使這些goroutines不阻塞。從而實現並行效果。
參考自: https://studygolang.com/articles/5463