golang ----並發 && 並行


 Go 語言的線程是並發機制,不是並行機制。

那么,什么是並發,什么是並行?

並發是不同的代碼塊交替執行,也就是交替可以做不同的事情。

並行是不同的代碼塊同時執行,也就是同時可以做不同的事情。

舉個生活化場景的例子:

你正在家看書,忽然電話來了,然后你接電話,通話完成后繼續看書,這就是並發,看書和接電話交替做。

如果電話來了,你一邊看書一遍接電話,這就是並行,看書和接電話一起做。

先看實例

package main

import (
	"fmt"
	"runtime"
	"sync"
)

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

  輸出:

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

 為什么會是這樣的結果?

並發不等於並行

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

雖然我們在for循環中使用了go 創建了一個goroutine,我們想當然會認為,每次循環變量時,golang一定會執行這個goroutine,然后輸出當時的變量。 這時,我們就陷入了思維定勢。 默認並發等於並行。

誠然,通過go創建的goroutine是會並發的執行其中的函數代碼。 但一定會按照我們所設想的那樣每次循環時執行嗎? 答案是否定的!

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,就可以看到隨機挑選的效果:

package main

import (
	"fmt"
	"runtime"
	"strconv"
	"strings"
	"sync"
)

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.Printf("go routine 1 : i -- %d  id-- %d\n ", 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.Printf("go routine 2 : i -- %d  id-- %d \n", i, id)
			wg.Done()
		}(i)

	}
	wg.Wait()
}

  輸出:

go routine 1 : i -- 10  id-- 20
 go routine 1 : i -- 10  id-- 19
 go routine 1 : i -- 10  id-- 22
 go routine 1 : i -- 10  id-- 25
 go routine 1 : i -- 10  id-- 28
 go routine 2 : i -- 8  id-- 37 
go routine 1 : i -- 10  id-- 23
 go routine 1 : i -- 10  id-- 21
 go routine 2 : i -- 0  id-- 29 
go routine 2 : i -- 9  id-- 38 
go routine 2 : i -- 1  id-- 30 
go routine 2 : i -- 5  id-- 34 
go routine 2 : i -- 2  id-- 31 
go routine 2 : i -- 6  id-- 35 
go routine 2 : i -- 3  id-- 32 
go routine 1 : i -- 10  id-- 24
 go routine 2 : i -- 7  id-- 36 
go routine 1 : i -- 10  id-- 27
 go routine 2 : i -- 4  id-- 33 
go routine 1 : i -- 10  id-- 26

 

我們再回到這道題中,雖然在循環中通過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依次輸出。

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM