天天看點

golang 并發goroutine詳解(二)

作者:幹飯人小羽
golang 并發goroutine詳解(二)

Go語言的并發和并行

不知道你有沒有注意到一個現象,還是這段代碼,如果我跑在兩個goroutines裡面的話:

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()

    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           

這是不是有什麼問題??

以前我們用線程去做類似任務的時候,系統的線程會搶占式地輸出, 表現出來的是亂序地輸出。而goroutine為什麼是這樣輸出的呢?

goroutine是在并行嗎?

我們找個例子測試下:

package main

import "fmt"
import "time"

var quit chan int

func foo(id int) {
    fmt.Println(id)
    time.Sleep(time.Second) // 停頓一秒
    quit <- 0 // 發消息:我執行完啦!
}


func main() {
    count := 1000
    quit = make(chan int, count) // 緩沖1000個資料

    for i := 0; i < count; i++ { //開1000個goroutine
        go foo(i)
    }

    for i :=0 ; i < count; i++ { // 等待所有完成消息發送完畢。
        <- quit
    }
}           

讓我們跑一下這個程式(之是以先編譯再運作,是為了讓程式跑的盡量快,測試結果更好):

go build test.go
time ./test
./test  0.01s user 0.01s system 1% cpu 1.016 total           

我們看到,總計用時接近一秒。 貌似并行了!

我們需要首先考慮下什麼是并發, 什麼是并行

并行和并發

從概念上講,并發和并行是不同的, 簡單來說看這個圖檔

golang 并發goroutine詳解(二)
  • 兩個隊列,一個Coffee機器,那是并發
  • 兩個隊列,兩個Coffee機器,那是并行

那麼回到一開始的疑問上,從上面的兩個例子執行後的表現來看,多個goroutine跑loop函數會挨個goroutine去進行,而sleep則是一起執行的。

這是為什麼?

預設地, Go所有的goroutines隻能在一個線程裡跑 。

也就是說, 以上兩個代碼都不是并行的,但是都是是并發的。

如果目前goroutine不發生阻塞,它是不會讓出CPU給其他goroutine的, 是以例子一中的輸出會是一個一個goroutine進行的,而sleep函數則阻塞掉了 目前goroutine, 目前goroutine主動讓其他goroutine執行, 是以形成了邏輯上的并行, 也就是并發。

真正的并行

為了達到真正的并行,我們需要告訴Go我們允許同時最多使用多個核。

回到起初的例子,我們設定最大開2個原生線程, 我們需要用到runtime包(runtime包是goroutine的排程器):

import (
    "fmt"
    "runtime"
)

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
    }
}           

這下會看到兩個goroutine會搶占式地輸出資料了。

我們還可以這樣顯式地讓出CPU時間:

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包來手工排程。

本文開頭的兩個例子都是限制在單核CPU裡執行的,所有的goroutines跑在一個線程裡面,分析如下:

  • 對于代碼例子一(loop函數的那個),每個goroutine沒有發生堵塞(直到quit流入資料), 是以在quit之前每個goroutine不會主動讓出CPU,也就發生了串行列印
  • 對于代碼例子二(time的那個),每個goroutine在sleep被調用的時候會阻塞,讓出CPU, 是以例子二并發執行。