1. 程式人生 > 其它 >Goroutine 排程器

Goroutine 排程器

Goroutine 排程器

Goroutine 佔用的資源非常小,每個 Goroutine 棧的大小預設是 2KB。而且,Goroutine 排程的切換也不用陷入(trap)作業系統核心層完成,代價很低。因此,一個 Go 程式中可以建立成千上萬個併發的 Goroutine。而將這些 Goroutine 按照一定演算法放到“CPU”上執行的程式,就被稱為 Goroutine 排程器(Goroutine Scheduler)

其任務:將 Goroutine 按照一定演算法放到不同的作業系統執行緒中去執行

Goroutine 排程器模型與演化過程

G-M 模型

2012 年 3 月 28 日,Go 1.0版本

G(Goroutine)、M(machine)

排程器的工作就是將 G 排程到 M 上去執行。為了更好地控制程式中活躍的 M 的數量,排程器引入了 GOMAXPROCS 變數來表示 Go 排程器可見的“處理器”的最大數量。

問題:限制了 Go 併發程式的伸縮性,尤其是對那些有高吞吐或平行計算需求的服務程式

體現在以下幾個方面:

  • 單一全域性互斥鎖(Sched.Lock) 和集中狀態儲存的存在,導致所有 Goroutine 相關操作,比如建立、重新排程等,都要上鎖;
  • Goroutine 傳遞問題:M 經常在 M 之間傳遞“可執行”的 Goroutine,這導致排程延遲增大,也增加了額外的效能損耗;
  • 每個 M 都做記憶體快取,導致記憶體佔用過高,資料區域性性較差;
  • 由於系統呼叫(syscall)而形成的頻繁的工作執行緒阻塞和解除阻塞,導致額外的效能損耗。

G-P-M 排程模型和work stealing 演算法

電腦科學領域的任何問題都可以通過增加一個間接的中間層來解決

通過向 G-M 模型中增加了一個 P(邏輯 Proccessor),讓 Go 排程器具有很好的伸縮性。

每個 G(Goroutine)要想真正執行起來,首先需要被分配一個 P,也就是進入到 P 的本地執行佇列(local runq)中。對於 G 來說,P 就是執行它的“CPU”,可以說:在 G 的眼裡只有 P。但從 Go 排程器的視角來看,真正的“CPU”是 M,只有將 P 和 M 繫結,才能讓 P 的 runq 中的 G 真正執行起來。

work-stealing 未充分利用的處理器會主動去尋找其他處理器的執行緒並 竊取 一些.

基於協作的“搶佔式”排程

Go 1.2

Go 編譯器在每個函式或方法的入口處加上了一段額外的程式碼 (runtime.morestack_noctxt),讓執行時有機會在這段程式碼中檢查是否需要執行搶佔排程。

問題:區域性解決了“餓死”問題,只在有函式呼叫的地方才能插入“搶佔”程式碼(埋點),對於沒有函式呼叫而是純演算法迴圈計算的 G,Go 排程器依然無法搶佔。

非協作的搶佔式排程

Go 1.14

這種搶佔式排程是基於系統訊號的,也就是通過向執行緒傳送訊號的方式來搶佔正在執行的 Goroutine。