【譯文】原文地址
本文是基于Go 1.13版本
創(chuàng)建一個(gè)操作系統(tǒng)線程或從一個(gè)線程切換到另一個(gè)線程,對程序來說在內(nèi)存和性能方面是代價(jià)很大的。Go為了充分利用cpu核的優(yōu)勢,在一開始就考慮了并發(fā)性。
M, P, G組合
為了解決性能問題,Go有自己的調(diào)度器來分配goroutine到線程上去。該調(diào)度器定義了三個(gè)主要概念,就如代碼中所表示的:
G-指的是goroutine
M-工作線程,或者機(jī)器
P-處理器,執(zhí)行代碼需要的資源。M必須有相應(yīng)的P才能運(yùn)行代碼。
如下是M, P, G模型圖:

每個(gè)goroutine(G)運(yùn)行在一個(gè)OS線程上,這個(gè)線程會(huì)被分配到一個(gè)邏輯CPU(P)。通過一個(gè)例子來看下Go如何管理它們的:
package main
import "sync"
func main() {
var wg sync.WaitGroup
wg.Add(2)
go func() {
println(`hello`)
wg.Done()
}()
go func() {
println(`world`)
wg.Done()
}()
wg.Wait()
}
Go首先將根據(jù)機(jī)器的邏輯cpu個(gè)數(shù)來創(chuàng)建不同的P,并存放在一個(gè)空閑的P列表中:

然后,新的一個(gè)goroutine或多個(gè)goroutine將喚醒P來更好的分配工作。這個(gè)P將創(chuàng)建一個(gè)與操作系統(tǒng)線程相關(guān)的M:

然而,空閑的P和M-即沒有等待執(zhí)行的goroutine,從系統(tǒng)調(diào)用中返回或甚至被垃圾收集器強(qiáng)制停止,將進(jìn)入空閑隊(duì)列中:

在程序的啟動(dòng)過程中,Go已經(jīng)創(chuàng)建了一些OS線程和相關(guān)的M。在以上例子中,第一個(gè)輸出hello的goroutine將使用main goroutine,而第二個(gè)goroutine將從空閑列表中獲取一個(gè)M和P:

以上我們看到了goroutine和線程的管理,下面看下在什么情況下Go會(huì)使用更多的M而不是P以及在系統(tǒng)調(diào)用下goroutine是如何管理的。
系統(tǒng)調(diào)用
Go優(yōu)化系統(tǒng)調(diào)用的方法是通過在運(yùn)行時(shí)對其進(jìn)行包裝,無論系統(tǒng)調(diào)用是否阻塞。這個(gè)包裝器將自動(dòng)將P和線程M分離,并允許其他線程在其上運(yùn)行。讓我們舉一個(gè)讀取文件的例子:
package main
import (
"os"
)
func main() {
buf := make([]byte, 0, 2)
fd, _ := os.Open("main.go")
fd.Read(buf)
fd.Close()
println(string(buf)) // 42
}
下面是文件打開的工作流程:

P0進(jìn)入空閑隊(duì)列,可供其他線程使用。一旦系統(tǒng)調(diào)用退出,Go將應(yīng)用如下滿足條件的規(guī)則:
- 嘗試獲取對應(yīng)的P,本例中的p0,并繼續(xù)執(zhí)行。
- 嘗試從空閑列表中獲取一個(gè)新的P,恢復(fù)執(zhí)行。
- 將正在執(zhí)行的goroutine放入全局隊(duì)列,并將對于的M放回空閑隊(duì)列。
然而,Go也可以處理資源尚未就緒的情況,比如http調(diào)用這種非阻塞I/O情況。在這種情況下,第一個(gè)系統(tǒng)調(diào)用(遵循前面的共工作流程)因?yàn)橘Y源未就緒將調(diào)用不成功,迫使Go使用網(wǎng)絡(luò)輪詢并使goroutine駐留。
func main() {
http.Get(`https://httpstat.us/200`)
}
一旦第一個(gè)系統(tǒng)調(diào)用完成并顯示資源未就緒,對應(yīng)的goroutine就會(huì)駐留,直到網(wǎng)絡(luò)輪詢通知資源就緒。在這種情況下,線程M將不會(huì)被阻塞:

當(dāng)Go調(diào)度器尋找工作時(shí),對應(yīng)的goroutine將再次運(yùn)行。然后調(diào)度器將詢問網(wǎng)絡(luò)輪詢器,是否有等待就緒的gorotine需要執(zhí)行。

如果就緒的goroutine不止一個(gè)的話,多出的goroutine將進(jìn)入全局可執(zhí)行隊(duì)列等待調(diào)度。
操作系統(tǒng)線程約束
當(dāng)使用系統(tǒng)調(diào)用時(shí),Go不限制被阻塞的操作系統(tǒng)線程數(shù)量,如下所述:
GOMAXPROCS變量限制了能夠同時(shí)執(zhí)行用戶態(tài)Go代碼的操作系統(tǒng)線程數(shù)量。但并沒有對Go代碼中處于阻塞狀態(tài)下系統(tǒng)調(diào)用的線程數(shù)量有限制。這些系統(tǒng)調(diào)用線程數(shù)量不會(huì)受影響。
以下就是這種情況的一個(gè)例子:
package main
import (
"fmt"
"net/http"
"sync"
)
func main() {
var wg sync.WaitGroup
for i := 0;i < 100 ;i++ {
wg.Add(1)
go func(i int) {
http.Get(`https://www.baidu.com`)
fmt.Println(i)
wg.Done()
}(i)
}
wg.Wait()
}
如下是跟蹤工具查看到的線程個(gè)數(shù):

因?yàn)镚o優(yōu)化了線程的使用,當(dāng)goroutine被阻塞時(shí)線程會(huì)被復(fù)用,這就解釋了為什么這個(gè)線程數(shù)和循環(huán)數(shù)量不一致。