天天看點

Go語言中的并發程式設計一、并發與并行二、goroutinegoroutine與線程channel

一、并發與并行

并發:同一時間段内執行多個任務

并行:同一時刻執行多個任務

Go語言的并發通過

goroutine

實作。

goroutine

類似于線程,屬于使用者态的線程,我們可以根據需要建立成千上萬個

goroutine

并發工作。

goroutine

是由Go語言的運作時(runtime)排程完成,而線程是由作業系統排程完成。

Go語言還提供

channel

在多個

goroutine

間進行通信。

goroutine

channel

是 Go 語言秉承的 CSP(Communicating Sequential Process)并發模式的重要實作基礎。

二、goroutine

Go語言中的

goroutine

就是這樣一種機制,

goroutine

的概念類似于線程,但 

goroutine

是由Go的運作時(runtime)排程和管理的。Go程式會智能地将 goroutine 中的任務合理地配置設定給每個CPU。Go語言之是以被稱為現代化的程式設計語言,就是因為它在語言層面已經内置了排程和上下文切換的機制。

在Go語言程式設計中你不需要去自己寫程序、線程、協程,你的技能包裡隻有一個技能–

goroutine

,當你需要讓某個任務并發執行的時候,你隻需要把這個任務包裝成一個函數,開啟一個

goroutine

去執行這個函數就可以了,就是這麼簡單粗暴。

使用goroutine

Go語言中使用

goroutine

非常簡單,隻需要在調用函數的時候在前面加上

go

關鍵字,就可以為一個函數建立一個

goroutine

一個

goroutine

必定對應一個函數,可以建立多個

goroutine

去執行相同的函數。

啟動單個goroutine

啟動goroutine的方式非常簡單,隻需要在調用的函數(普通函數和匿名函數)前面加上一個

go

關鍵字。

舉個例子如下:

func hello() {
	fmt.Println("Hello Goroutine!")
}
func main() {
	hello()
	fmt.Println("main goroutine done!")
}
           

這個示例中hello函數和下面的語句是串行的,執行的結果是列印完

Hello Goroutine!

後列印

main goroutine done!

接下來我們在調用hello函數前面加上關鍵字

go

,也就是啟動一個goroutine去執行hello這個函數。

func main() {
	go hello() // 啟動另外一個goroutine去執行hello函數
	fmt.Println("main goroutine done!")
}
           

這一次的執行結果隻列印了

main goroutine done!

,并沒有列印

Hello Goroutine!

。為什麼呢?

在程式啟動時,Go程式就會為

main()

函數建立一個預設的

goroutine

當main()函數傳回的時候該

goroutine

就結束了,所有在

main()

函數中啟動的

goroutine

會一同結束,

main

函數所在的

goroutine

就像是權利的遊戲中的夜王,其他的

goroutine

都是異鬼,夜王一死它轉化的那些異鬼也就全部GG了。

是以我們要想辦法讓main函數等一等hello函數,最簡單粗暴的方式就是

time.Sleep

了。

func main() {
	go hello() // 啟動另外一個goroutine去執行hello函數
	fmt.Println("main goroutine done!")
	time.Sleep(time.Second)
}
           

執行上面的代碼你會發現,這一次先列印

main goroutine done!

,然後緊接着列印

Hello Goroutine!

首先為什麼會先列印

main goroutine done!

是因為我們在建立新的goroutine的時候需要花費一些時間,而此時main函數所在的

goroutine

是繼續執行的。

啟動多個goroutine

在Go語言中實作并發就是這樣簡單,我們還可以啟動多個

goroutine

。讓我們再來一個例子: (這裡使用了

sync.WaitGroup

來實作goroutine的同步)

var wg sync.WaitGroup

func hello(i int) {
	defer wg.Done() // goroutine結束就登記-1
	fmt.Println("Hello Goroutine!", i)
}
func main() {

	for i := 0; i < 10; i++ {
		wg.Add(1) // 啟動一個goroutine就登記+1
		go hello(i)
	}
	wg.Wait() // 等待所有登記的goroutine都結束
}
           

多次執行上面的代碼,會發現每次列印的數字的順序都不一緻。這是因為10個

goroutine

是并發執行的,而

goroutine

的排程是随機的。

goroutine與線程

可增長的棧

OS線程(作業系統線程)一般都有固定的棧記憶體(通常為2MB),一個

goroutine

的棧在其生命周期開始時隻有很小的棧(典型情況下2KB),

goroutine

的棧不是固定的,他可以按需增大和縮小,

goroutine

的棧大小限制可以達到1GB,雖然極少會用到這麼大。是以在Go語言中一次建立十萬左右的

goroutine

也是可以的。

goroutine排程

GPM

是Go語言運作時(runtime)層面的實作,是go語言自己實作的一套排程系統。差別于作業系統排程OS線程。

  • G

    很好了解,就是個goroutine的,裡面除了存放本goroutine資訊外 還有與所在P的綁定等資訊。
  • P

    管理着一組goroutine隊列,P裡面會存儲目前goroutine運作的上下文環境(函數指針,堆棧位址及位址邊界),P會對自己管理的goroutine隊列做一些排程(比如把占用CPU時間較長的goroutine暫停、運作後續的goroutine等等)當自己的隊列消費完了就去全局隊列裡取,如果全局隊列裡也消費完了會去其他P的隊列裡搶任務。
  • M(machine)

    是Go運作時(runtime)對作業系統核心線程的虛拟, M與核心線程一般是一一映射的關系, 一個groutine最終是要放到M上執行的;

P與M一般也是一一對應的。他們關系是: P管理着一組G挂載在M上運作。當一個G長久阻塞在一個M上時,runtime會建立一個M,阻塞G所在的P會把其他的G 挂載在建立的M上。當舊的G阻塞完成或者認為其已經死掉時 回收舊的M。

P的個數是通過

runtime.GOMAXPROCS

設定(最大256),Go1.5版本之後預設為實體線程數。 在并發量大的時候會增加一些P和M,但不會太多,切換太頻繁的話得不償失。

單從線程排程講,Go語言相比起其他語言的優勢在于OS線程是由OS核心來排程的,

goroutine

則是由Go運作時(runtime)自己的排程器排程的,這個排程器使用一個稱為m:n排程的技術(複用/排程m個goroutine到n個OS線程)。 其一大特點是goroutine的排程是在使用者态下完成的, 不涉及核心态與使用者态之間的頻繁切換,包括記憶體的配置設定與釋放,都是在使用者态維護着一塊大的記憶體池, 不直接調用系統的malloc函數(除非記憶體池需要改變),成本比排程OS線程低很多。 另一方面充分利用了多核的硬體資源,近似的把若幹goroutine均分在實體線程上, 再加上本身goroutine的超輕量,以上種種保證了go排程方面的性能。

了解更多

GOMAXPROCS

Go運作時的排程器使用

GOMAXPROCS

參數來确定需要使用多少個OS線程來同時執行Go代碼。預設值是機器上的CPU核心數。例如在一個8核心的機器上,排程器會把Go代碼同時排程到8個OS線程上(GOMAXPROCS是m:n排程中的n)。

Go語言中可以通過

runtime.GOMAXPROCS()

函數設定目前程式并發時占用的CPU邏輯核心數。

Go1.5版本之前,預設使用的是單核心執行。Go1.5版本之後,預設使用全部的CPU邏輯核心數。

我們可以通過将任務配置設定到不同的CPU邏輯核心上實作并行的效果,這裡舉個例子:

func a() {
	for i := 1; i < 10; i++ {
		fmt.Println("A:", i)
	}
}

func b() {
	for i := 1; i < 10; i++ {
		fmt.Println("B:", i)
	}
}

func main() {
	runtime.GOMAXPROCS(1)
	go a()
	go b()
	time.Sleep(time.Second)
}
           

兩個任務隻有一個邏輯核心,此時是做完一個任務再做另一個任務。 将邏輯核心數設為2,此時兩個任務并行執行,代碼如下。

func a() {
	for i := 1; i < 10; i++ {
		fmt.Println("A:", i)
	}
}

func b() {
	for i := 1; i < 10; i++ {
		fmt.Println("B:", i)
	}
}

func main() {
	runtime.GOMAXPROCS(2)
	go a()
	go b()
	time.Sleep(time.Second)
}
           

Go語言中的作業系統線程和goroutine的關系:

  1. 一個作業系統線程對應使用者态多個goroutine。
  2. go程式可以同時使用多個作業系統線程。
  3. goroutine和OS線程是多對多的關系,即m:n。

channel