Go Scheduler 的 GMP 模型

閃念基因 發佈 2024-04-28T23:15:01.478030+00:00

寫在前面Go 為了自身 goroutine 執行和調度的效率,自身在 runtime 中實現了一套 goroutine 的調度器,下面通過一段簡單的代碼展示一下 Go 應用程式在運行時的 goroutine,方便大家更好的理解。

寫在前面

Go 為了自身 goroutine 執行和調度的效率,自身在 runtime 中實現了一套 goroutine 的調度器,下面通過一段簡單的代碼展示一下 Go 應用程式在運行時的 goroutine,方便大家更好的理解。

The Go scheduler is part of the Go runtime, and the Go runtime is built into your applicatIOn

for i := 0; i < 4; i++ {
        go func() {
                time.Sleep(time.Second)
        }()
}
fmt.Println(runtime.NumGoroutine())

上面這段代碼的輸出為:5。說明當前這個應用程式中存在 goroutine 的數量是 5,事實上也符合我們的預期。那麼問題來了,這 5 個 goroutine 作為作業系統用戶態的基本調度單元是無法直接占用作業系統的資源來執行的,必須經過內核級線程的分發,這是作業系統內部線程調度的基本模型,根據用戶級線程和內核級線程的對應關係可以分為 1 對 1,N 對 1 以及 M 對 N 這三種模型,那麼上述的 5 個 goroutine 在內核級線程上是怎麼被分發的,這就是 Go語言的 goroutine 調度器決定的。

GMP 模型

整個 goroutine 調度器的實現基於 GMP 的三級模型來實現。

  • G:goroutine
  • M:內核級線程,運行在作業系統的核心態。在 Go 中支持最大的 M 的數量是 10000,但是作業系統中通常情況是不可以創建這麼多的線程。
  • P:processor,可以理解成一個等待分發給 M 調度執行的 goroutine 隊列。P的個數是由 runtime 的 GOMAXPROCS 來決定的。

M 和 P 存在一一對應的綁定關係。大致的結構圖如下所示:

goroutine 之旅

通常情況下,我們在代碼中執行 go func(){}後,GMP 模型是如何工作的?通過一個詳細的圖來展示一下。

  1. 首先創建一個新的 goroutine
  2. 如果本地的局部隊列中有足夠的空間可以存放,則放入局部隊列中;如果局部隊列滿,則放入一個全局隊列(所有的 M 都可以從全局隊列中拉取 G 來執行)
  3. 所有的 G 都必須在 M 上才可以被執行,M 和 P 存在一一綁定的關係,如果 M 綁定的 P 中存在可以被執行的 G,則從 P 中拉取 G 來執行;如果 P 中為空,沒有可執行的 G,則 M 從全局隊列中拉取;如果全局隊列也為空,則從其他的 P 中拉取 G
  4. 為 G 的運行分配必要的資源,等待 CPU 的調度
  5. 分配到 CPU,執行 func(){}

調度策略

整個 goroutine 調度器最重要的調度策略是:復用,避免頻繁的資源創建和銷毀,最大限度的提升系統的吞吐量和並發程度。這也是作業系統進行線程調度的終極目標。復用(reuse)也是很多「池化技術」的基礎。

圍繞著這一原則,goroutine 調度器在以下幾個方面進行調度策略的優化。

  1. 工作隊列的竊取機制:這個跟 Java 中的 ForkJoin Pool 的竊取機制同一原理,都是當線程 M 空閒時,從其他繁忙的隊列 P 中"竊取"任務 G 過來執行,而不是銷毀空閒的 M。因為線程的創建和銷毀是需要消耗系統資源的,避免線程的頻繁創建和銷毀可以極大的提升系統的並發程度。
  2. 交接機制:當線程M被阻塞的時候,M 會主動將 P 交接給其他空閒的 M。

另外,在 go 的 1.14 版本中,go 語言的技術團隊嘗試在調度器中添加了可搶占的技術[https://github.com/golang/go/issues/24543]

  1. 搶占技術的出現一方面解決了線程 M 在執行計算密集型任務時長時間占用 CPU,導致與之綁定的 P 上的其他 G 得不到執行而造成的"飢餓現象";
  2. 另一方面,搶占技術的出現對 GC 來講解決 GC 時可能出現的 deadLock,相關的 issue 見:關於 GC 時 tight loops 應該可以被搶占的討論
  3. [https://github.com/golang/go/issues/10958]

最開始的 MG 模型

在 go 語言的早期,goroutine 調度器的模型並不是 GMP,而是 GM。整個調度器維護一個全局的 G 的等待隊列,所有的 M 從這個全局的隊列中拉取 G 來執行,在 go1.1 中將這種模型直接幹掉,取而代之的是現在的 GMP 模型,在 GM 模型的基礎上增加 P 局部隊列。官方之所有這麼這麼做,原因有三:

  1. 全局的 G 等待隊列,不同的M從隊列里取 G 都需要加鎖,鎖的粒度很大,嚴重製約了系統並發能力的提升;
  2. 沒有局部隊列,那麼當線程在執行 IO 密集型操作時,M 阻塞在 IO 操作上,對應的 G 也沒有辦法得到執行(GMP 中可以將 G 交接給其他的 M 執行),因此 GM 模型在應對 IO 密集型任務時性能表現低下

作者:程翔龍

來源:微信公眾號:字節跳動技術團隊

出處:https://mp.weixin.qq.com/s/1CY3E5daJ5U42orVwzCpaw

關鍵字: