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。