go的并發實現采用的是M:N的線程模型,落地就是gmp模型。
M:N模型如下圖:
gmp模型如下圖:
---
Go 的 GMP 模型是其 高效并發調度機制的核心。GMP 代表:
-
G:Goroutine(用戶態線程)
-
M:Machine(綁定內核線程)
-
P:Processor(調度器/執行上下文)
Go 通過這三個組件,實現了 goroutine 的調度和執行,避免了頻繁的線程創建與上下文切換,性能優秀。
那么gmp模型是怎么實現的呢?
多個 Goroutine (G) -> 由 P 管理調度 -> 由 M(線程)實際執行;
具體來,需要執行的G是放在P的隊列里面等著被執行調用的,不過有時候會有一些岔子,為了保證M的使用率,會有一些具體的調度算法,讓G被調來調取,大概情況是:
-
Hand off:比較重的調度;M阻塞了(syscall),就把M手頭的G收走,讓其他M去執行;
-
Work Stealing:M對于的P中的隊列沒有G了,從其他地方調一些來
-
普通調度:G1阻塞了,例如sleep,io了,直接把G1掛起,讓其他G被M執行。
-
等等
head off可以用圖片來理解:
Hand off圖源
整體的調度思路可以用偽代碼來理解:
?// G = Goroutine,代表一個用戶級線程(任務)// M = Machine,代表一個工作線程(對應一個內核線程)// P = Processor,代表執行資源(運行隊列+執行上下文),M 必須綁定 P 才能運行 G?type G struct {fn func() ? ?// Goroutine 要執行的函數}?type M struct {p *P ? ? ? ? // 當前綁定的 Pcurg *G ? ? ?// 當前正在執行的 G// ... 還有調用棧等}?type P struct {runQueue []*G ? ?// 本地 G 隊列// 還包括調度器上下文、調度時間等}?// 系統初始化時,創建 GOMAXPROCS 個 P,通常等于 CPU 核數func initRuntime() {for i := 0; i < GOMAXPROCS; i++ {allP[i] = new(P)}// 啟動第一個 MstartM()}?// 啟動一個 M(內核線程),從全局找可用的 P,然后調度func startM() {m := new(M)m.p = acquireP() ? // 找一個空閑 Pgo m.run() ? ? ? ? // 啟動內核線程,進入調度循環}?// M 的主循環,持續運行 Gfunc (m *M) run() {for {g := m.p.findRunnableG() // 找到一個可運行的 Gif g == nil {// 若本地隊列空了,可以嘗試 steal 其他 P 的 Gg = stealFromOtherP()if g == nil {// 若仍找不到,當前 M 休眠stopM(m)return}}m.curg = grunG(g) // 運行 G 的函數m.curg = nil}}?// P 的調度器,從本地 runQueue 中找 goroutinefunc (p *P) findRunnableG() *G {if len(p.runQueue) == 0 {return nil}g := p.runQueue[0]p.runQueue = p.runQueue[1:]return g}?// 當調用 go f() 時,生成一個新的 G,并放入當前 P 的隊列func goNew(f func()) {g := &G{fn: f}curP := currentM().pcurP.runQueue = append(curP.runQueue, g)// 若當前 M 忙不過來,可觸發 newM 讓新線程幫忙跑 G}
?
參考資料:
深入淺出 Go 語言 GMP 模型?https://juejin.cn/post/7434518199234740233
劉丹冰?【Golang深入理解GPM模型】https://www.bilibili.com/video/BV19r4y1w7Nx/?share_source=copy_web&vd_source=4ab2dac702abaae48d1782021ca7150c