并發(fā)不等于并行
golang的核心開發(fā)人員Rob Pike專門提到了這個(gè)話題 (推薦學(xué)習(xí):go)
雖然我們?cè)趂or循環(huán)中使用了go 創(chuàng)建了一個(gè)goroutine,我們想當(dāng)然會(huì)認(rèn)為,每次循環(huán)變量時(shí),golang一定會(huì)執(zhí)行這個(gè)goroutine,然后輸出當(dāng)時(shí)的變量。
這時(shí),我們就陷入了思維定勢(shì)。 默認(rèn)并發(fā)等于并行。
誠(chéng)然,通過(guò)go創(chuàng)建的goroutine是會(huì)并發(fā)的執(zhí)行其中的函數(shù)代碼。
但一定會(huì)按照我們所設(shè)想的那樣每次循環(huán)時(shí)執(zhí)行嗎?
答案是否定的!
Rob Pike專門提到了golang中并發(fā)指的是代碼結(jié)構(gòu)中的某些函數(shù)邏輯上可以同時(shí)運(yùn)行,但物理上未必會(huì)同時(shí)運(yùn)行。而并行則指的就是在物理層面也就是使用了不同CPU在執(zhí)行不同或者相同的任務(wù)。
golang的goroutine調(diào)度模型決定了,每個(gè)goroutine是運(yùn)行在虛擬CPU中的(也就是我們通過(guò)runtime.GOMAXPROCS(1)所設(shè)定的虛擬CPU個(gè)數(shù))。
虛擬CPU個(gè)數(shù)未必會(huì)和實(shí)際CPU個(gè)數(shù)相吻合。每個(gè)goroutine都會(huì)被一個(gè)特定的P(虛擬CPU)選定維護(hù),而M(物理計(jì)算資源)每次回挑選一個(gè)有效P,然后執(zhí)行P中的goroutine。
每個(gè)P會(huì)將自己所維護(hù)的goroutine放到一個(gè)G隊(duì)列中,其中就包括了goroutine堆棧信息,是否可執(zhí)行信息等等。默認(rèn)情況下,P的數(shù)量與實(shí)際物理CPU的數(shù)量相等。
因此當(dāng)我們通過(guò)循環(huán)來(lái)創(chuàng)建goroutine時(shí),每個(gè)goroutine會(huì)被分配到不同的P隊(duì)列中。而M的數(shù)量又不是唯一的,當(dāng)M隨機(jī)挑選P時(shí),也就等同隨機(jī)挑選了goroutine。
在本題中,我們?cè)O(shè)定了P=1。所以所有的goroutine會(huì)被綁定到同一個(gè)P中。 如果我們修改runtime.GOMAXPROCS的值,就會(huì)看到另外的順序。 如果我們輸出goroutine id,就可以看到隨機(jī)挑選的效果:
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
我們?cè)倩氐竭@道題中,雖然在循環(huán)中通過(guò)go定義了一個(gè)goroutine。但我們說(shuō)到了,并發(fā)不等于并行。因此雖然定義了,但此刻不見得就會(huì)去執(zhí)行。
需要等待M選擇P之后,才能去執(zhí)行g(shù)oroutine。 關(guān)于golang中g(shù)oroutine是如何進(jìn)行調(diào)度的(GPM模型),可以參考Scalable Go Scheduler Design Doc或者LearnConcurrency