Go語言的併發和並行

Go語言的併發和並行

不知道你有沒有注意到一個現象,仍是這段代碼,若是我跑在兩個goroutines裏面的話:html

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

咱們觀察下輸出:python

0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9

這是否是有什麼問題??git

之前咱們用線程去作相似任務的時候,系統的線程會搶佔式地輸出, 表現出來的是亂序地輸出。而goroutine爲何是這樣輸出的呢?程序員

goroutine是在並行嗎?

咱們找個例子測試下:github

package mainimport "fmt"import "time"var quit chan intfunc 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
    }}

讓咱們跑一下這個程序(之因此先編譯再運行,是爲了讓程序跑的儘可能快,測試結果更好):golang

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

咱們看到,總計用時接近一秒。 貌似並行了!segmentfault

咱們須要首先考慮下什麼是併發, 什麼是並行併發

並行和併發

從概念上講,併發和並行是不一樣的, 簡單來講看這個圖片(原圖來自這裏)ide

  • 兩個隊列,一個Coffee機器,那是併發函數

  • 兩個隊列,兩個Coffee機器,那是並行

更多的資料: 併發不是並行, 固然Google上有更多關於並行和併發的區別。

那麼回到一開始的疑問上,從上面的兩個例子執行後的表現來看,多個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致使了看上去的「並行」。

其實做爲一個Python程序員,goroutine讓我更多地想到的是gevent的協程,而不是原生線程。

關於runtime包對goroutine的調度,在stackoverflow上有一個不錯的答案:http://stackoverflow.com/questions/13107958/what-exactly-does-runtime-gosched-do

一個小問題

我在Segmentfault看到了這個問題: http://segmentfault.com/q/1010000000207474

題目說,以下的程序,按照理解應該打印下5次 "world"呀,但是爲何什麼也沒有打印

package mainimport (
    "fmt")func say(s string) {
    for i := 0; i < 5; i++ {
        fmt.Println(s)
    }}func main() {
    go say("world") //開一個新的Goroutines執行
    for {
    }}

樓下的答案已經很棒了,這裏Go仍然在使用單核,for死循環佔據了單核CPU全部的資源,而main線和say兩個goroutine都在一個線程裏面, 因此say沒有機會執行。解決方案仍是兩個:

  • 容許Go使用多核(runtime.GOMAXPROCS)

  • 手動顯式調動(runtime.Gosched)

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, 因此例子二併發執行。

那麼關於咱們開啓多核的時候呢?Go語言對goroutine的調度行爲又是怎麼樣的?

咱們能夠在Golang官方網站的這裏 找到一句話:

When a coroutine blocks, such as by calling a blocking system call, the run-time automatically moves other coroutines on the same operating system thread to a different, runnable thread so they won't be blocked.

也就是說:

當一個goroutine發生阻塞,Go會自動地把與該goroutine處於同一系統線程的其餘goroutines轉移到另外一個系統線程上去,以使這些goroutines不阻塞

開啓多核的實驗

仍然須要作一個實驗,來測試下多核支持下goroutines的對原生線程的分配, 也驗證下咱們所獲得的結論「goroutine不阻塞不放開CPU」。

實驗代碼以下:

package mainimport (
    "fmt"
    "runtime")var quit chan int = make(chan int)func loop(id int) { // id: 該goroutine的標號
    for i := 0; i < 10; i++ { //打印10次該goroutine的標號
        fmt.Printf("%d ", id)
    }
    quit <- 0}func main() {
    runtime.GOMAXPROCS(2) // 最多同時使用2個核

    for i := 0; i < 3; i++ { //開三個goroutine
        go loop(i)
    }

    for i := 0; i < 3; i++ {
        <- quit
    }}

多跑幾回會看到相似這些輸出(不一樣機器環境不同):

0 0 0 0 0 1 1 0 0 1 0 0 1 0 1 2 1 2 1 2 1 2 1 2 1 2 2 2 2 2
0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2
0 0 0 0 0 0 0 1 1 1 1 1 0 1 0 1 0 1 2 1 2 1 2 2 2 2 2 2 2 2
0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 0 2 0 2 0 2 2 2 2 2 2 2 2
0 0 0 0 0 0 0 1 0 0 1 0 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2 2 2

執行它咱們會發現如下現象:

  • 有時會發生搶佔式輸出(說明Go開了不止一個原生線程,達到了真正的並行)

  • 有時會順序輸出, 打印完0再打印1, 再打印2(說明Go開一個原生線程,單線程上的goroutine不阻塞不鬆開CPU)

那麼,咱們還會觀察到一個現象,不管是搶佔地輸出仍是順序的輸出,都會有那麼兩個數字表現出這樣的現象:

  • 一個數字的全部輸出都會在另外一個數字的全部輸出以前

緣由是, 3個goroutine分配到至多2個線程上,就會至少兩個goroutine分配到同一個線程裏,單線程裏的goroutine 不阻塞不放開CPU, 也就發生了順序輸出。

相關文章
相關標籤/搜索