Go 調度器的任務竊取(Work-Stealing)

Go 調度器的任務竊取(Work-Stealing)

<code>Illustration created for “A Journey With Go”, made from the original Go Gopher, created by Renee French.
/<code>

:information_source: 這篇文章基於 Go 1.13 環境。

在 Go 中創建 gorotine 既方便又快捷,然而 Go 在同一時間內最多在一個核上運行一個 gorotine,因此需要一種方法來存放其他的 gorotine,從而確保處理器(processor)負載均衡。

Goroutine 隊列

Go 使用兩級隊列來管理等待中的 goroutine,分別為本地隊列和全局隊列。每一個處理器都擁有本地隊列,而全局隊列是唯一的,且能被所有的處理器訪問到:

Go 調度器的任務竊取(Work-Stealing)

每個本地隊列都有最大容量,為 256。在容量滿了之後,任意新到來的 Goroutine 都會被放置到全局隊列。下面的例子是,生產了上千個 Goroutine 的程序:

<code>func main() {
var wg sync.WaitGroup

for i := 0;i < 2000 ;i++ {
wg.Add(1)
Go func() {
a := 0

for i := 0; i < 1e6; i++ {
a += 1
}

wg.Done()
}()
}

wg.Wait()
}/<code>

下面是擁有兩個處理器的調度器追蹤數據(traces):

Go 調度器的任務竊取(Work-Stealing)

追蹤數據通過 runqueue 展示了全局隊列中 Goroutine 的數量,以及方括號中 [3 256] 的本地隊列 goroutine 數量(分別為 P0 和 P1 )。當本地隊列滿了,積壓了 256 個等待中的 goroutine 後,下一個 Goroutine 會被壓棧到全局隊列中,正如我們從 runqueue 看到的數量增長一樣。

Goroutine 僅在本地隊列滿載之後才會加入到全局隊列;它也會在 Go 往調度器中批量注入時被加到全局隊列,例如,網絡輪詢器(network poller) 或者在垃圾回收期間等待的 goroutine。

下面是上一個例子的圖示:

Go 調度器的任務竊取(Work-Stealing)

不過,我們還想知道,為什麼本地隊列 P0 在上一個列子中不為空。因為 Go 使用了其他策略確保每個處理器都有任務處理。

任務竊取

如果處理器沒有任務可處理,它會按以下規則來執行,直到滿足某一條規則:

  • 從本地隊列獲取任務
  • 從全局隊列獲取任務
  • 從網絡輪詢器獲取任務
  • 從其它的處理器的本地隊列竊取任務

在我們前面的例子中,主函數在 P1 上運行並創建 goroutine。當第一批 gourinte 已經進入了 P1 的本地隊列時, P0 正在尋找任務。然而,它的本地隊列,全局隊列,以及網絡輪詢器都是空的。最後的解決方法是從 P1 中竊取任務。

Go 調度器的任務竊取(Work-Stealing)

下面是調度器在發生任務竊取前後的追蹤數據:

Go 調度器的任務竊取(Work-Stealing)

追蹤數據展示了,處理器是如何從其它處理器中竊取任務的。它從(其他處理器的)本地隊列中取走一半的 goroutine;在七個 Goroutine 中,偷走了四個 —— 其中一個立馬在 P0 執行,剩下的放到本地隊列。現在處理器間工作處於負載良好的狀態。這能通過執行 tracing 來確認:

Go 調度器的任務竊取(Work-Stealing)

goroutine 被合理地分發,然後因為沒有 I/O,goroutine 被鏈式執行而不需要切換。我們現在看一下,當出現例如涉及到文件操作等 I/O 時,會發生什麼。

I/O 與全局隊列

一起看下涉及到文件操作的例子:

<code>func main() {
var wg sync.WaitGroup

for i := 0;i < 20 ;i++ {
wg.Add(1)
Go func() {
a := 0
for i := 0; i < 1e6; i++ {
a += 1
if i == 1e6/2 {
bytes, _ := ioutil.ReadFile(`add.txt`)
inc, _ := strconv.Atoi(string(bytes))
a += inc
}
}
wg.Done()
}()
}

wg.Wait()
}/<code>

變量 a 隨著時間以文件的字節數增加,下面是新的追蹤數據:

Go 調度器的任務竊取(Work-Stealing)

在這個例子中,我們能看到每一個 Goroutine 不只被一個處理器處理。在系統調用的情況下,當調用完成後,Go 使用網絡輪詢器從全局隊列中把 gouroutine 取回來。這裡是 Goroutine #35 的一個示意圖:

Go 調度器的任務竊取(Work-Stealing)

當一個處理器能從全局隊列中獲取任務,第一個可用的處理器( P ) 會執行這個 goroutine。這個行為解釋了,為什麼一個 Goroutine 能在不同的處理器中運行,也展示了 Go 是如何讓空閒的處理器資源運行 goroutine,從而進行系統調用的優化。


分享到:


相關文章: