golang的併發不等於並行

轉自我的博客 chinazt.ccgit

先看下面一道面試題:github

func main() {
	runtime.GOMAXPROCS(1)
	wg := sync.WaitGroup{}
	wg.Add(20)
	for i := 0; i < 10; i++ {
		go func() {
			fmt.Println("go routine 1 i: ", i)
			wg.Done()
		}()
	}
	for i := 0; i < 10; i++ {
		go func(i int) {
			fmt.Println("go routine 2 i: ", i)
			wg.Done()
		}(i)

	}
	wg.Wait()
}

在不執行代碼的前提下,腦補一下輸出結果應該是什麼。golang

我再看到這道題時,首先想到輸出應該是0 -- 9 依次輸出。 但執行後才大跌眼鏡,錯的不是一點半點。首先看一下,在我本地執行的結果:面試

go routine 2 i: 9
go routine 1 i: 10
go routine 1 i: 10
go routine 1 i: 10
go routine 1 i: 10
go routine 1 i: 10
go routine 1 i: 10
go routine 1 i: 10
go routine 1 i: 10
go routine 1 i: 10
go routine 1 i: 10
go routine 2 i: 0
go routine 2 i: 1
go routine 2 i: 2
go routine 2 i: 3
go routine 2 i: 4
go routine 2 i: 5
go routine 2 i: 6
go routine 2 i: 7
go routine 2 i: 8

意不意外? 驚不驚喜?併發

爲何會是這樣的結果, 再翻閱了google官方出品的golang文檔以後,總算搞到了一些頭緒。ide

併發不等於並行

golang的核心開發人員Rob Pike專門提到了這個話題(有興趣能夠看這個視頻或者看原文PPT)函數

雖然咱們在for循環中使用了go 建立了一個goroutine,咱們想固然會認爲,每次循環變量時,golang必定會執行這個goroutine,而後輸出當時的變量。 這時,咱們就陷入了思惟定勢。 默認併發等於並行。google

誠然,經過go建立的goroutine是會併發的執行其中的函數代碼。 但必定會按照咱們所設想的那樣每次循環時執行嗎? 答案是否認的!code

Rob Pike專門提到了golang中併發指的是代碼結構中的某些函數邏輯上能夠同時運行,但物理上未必會同時運行。而並行則指的就是在物理層面也就是使用了不一樣CPU在執行不一樣或者相同的任務。視頻

golang的goroutine調度模型決定了,每一個goroutine是運行在虛擬CPU中的(也就是咱們經過runtime.GOMAXPROCS(1)所設定的虛擬CPU個數)。 虛擬CPU個數未必會和實際CPU個數相吻合。每一個goroutine都會被一個特定的P(虛擬CPU)選定維護,而M(物理計算資源)每次回挑選一個有效P,而後執行P中的goroutine。

每一個P會將本身所維護的goroutine放到一個G隊列中,其中就包括了goroutine堆棧信息,是否可執行信息等等。默認狀況下,P的數量與實際物理CPU的數量相等。所以當咱們經過循環來建立goroutine時,每一個goroutine會被分配到不一樣的P隊列中。而M的數量又不是惟一的,當M隨機挑選P時,也就等同隨機挑選了goroutine。

在本題中,咱們設定了P=1。因此全部的goroutine會被綁定到同一個P中。 若是咱們修改runtime.GOMAXPROCS的值,就會看到另外的順序。 若是咱們輸出goroutine id,就能夠看到隨機挑選的效果:

func main() {
	wg := sync.WaitGroup{}
	wg.Add(20)
	for i := 0; i < 10; i++ {
		go func() {
			var buf [64]byte
			n := runtime.Stack(buf[:], false)
			idField := strings.Fields(strings.TrimPrefix(string(buf[:n]), "goroutine "))[0]
			id, err := strconv.Atoi(idField)
			if err != nil {
				panic(fmt.Sprintf("cannot get goroutine id: %v", err))
			}
			fmt.Println("go routine 1 i: ", i, id)
			wg.Done()
		}()
	}
	for i := 0; i < 10; i++ {
		go func(i int) {
			var buf [64]byte
			n := runtime.Stack(buf[:], false)
			idField := strings.Fields(strings.TrimPrefix(string(buf[:n]), "goroutine "))[0]
			id, err := strconv.Atoi(idField)
			if err != nil {
				panic(fmt.Sprintf("cannot get goroutine id: %v", err))
			}
			fmt.Println("go routine 2 i: ", i, id)
			wg.Done()
		}(i)

	}
	wg.Wait()
}

輸出以下:

go routine 2 i: 9 24
go routine 1 i: 10 11
go routine 1 i: 10 5
go routine 1 i: 10 6
go routine 2 i: 3 18
go routine 1 i: 10 9
go routine 1 i: 10 10
go routine 1 i: 10 8
go routine 2 i: 0 15
go routine 2 i: 4 19
go routine 2 i: 6 21
go routine 1 i: 10 7
go routine 1 i: 10 14
go routine 2 i: 7 22
go routine 2 i: 8 23
go routine 1 i: 10 13
go routine 2 i: 5 20
go routine 1 i: 10 12
go routine 2 i: 1 16
go routine 2 i: 2 17

⋊> ~/S/g/g/s/t/C/goroutine ./goroutine
go routine 1 i: 10 11
go routine 2 i: 9 24
go routine 1 i: 10 6
go routine 1 i: 10 14
go routine 1 i: 10 9
go routine 1 i: 10 10
go routine 1 i: 10 12
go routine 2 i: 0 15
go routine 1 i: 10 13
go routine 1 i: 10 5
go routine 2 i: 1 16
go routine 2 i: 5 20
go routine 1 i: 10 7
go routine 2 i: 7 22
go routine 2 i: 3 18
go routine 2 i: 2 17
go routine 2 i: 4 19
go routine 1 i: 10 8
go routine 2 i: 8 23
go routine 2 i: 6 21

咱們再回到這道題中,雖然在循環中經過go定義了一個goroutine。但咱們說到了,併發不等於並行。所以雖然定義了,但此刻不見得就會去執行。須要等待M選擇P以後,才能去執行goroutine。 關於golang中goroutine是如何進行調度的(GPM模型),能夠參考Scalable Go Scheduler Design Doc或者LearnConcurrency

這時應該就能夠理解爲何會先輸出goroutine2而後再輸出goroutine1了吧。

下面咱們來解釋爲何goroutine1中輸出的都是10.

goroutine如何綁定變量

在golang的for循環中,golang每次都使用相同的變量實例(也就是題中所使用的i)。 而golang之間是共享環境變量的。

當調度到這個goroutine時,它就直接讀取所保存的變量地址,此時就會出現一個問題:goroutine保存的只是變量地址,因此變量是有可能被修改的

再結合題中的for循環,每次使用的都是同一個變量地址,也就是說i每次都在變化,到循環結束之時,i就變成了10. 而goroutine中保存的也只有i的內存地址而已,因此當goroutine1執行時,堅決果斷的就把i的內容讀了出來,多少呢? 10!

但爲何goroutine2不是10呢?

反過來看goroutine2,就容易理解了。由於在每次循環中都從新生成了一個新變量,而後每一個goroutine保存的是各自新變量的地址。 這些變量相互之間互不干擾,不會被任何人所篡改。所以在輸出時,會從0 - 9依次輸出。

其實這些問題,golang官方已經發過預警提示。 只管本身看官方文檔的習慣,因此直接栽坑裏了。

好在及時發現了本身的不足,亡羊補牢,爲時未晚吧。

相關文章
相關標籤/搜索