都說 Go 可以開啟成千上萬的 Goroutine,那調(diào)度器是怎么處理核上任務(wù)分配的?

Illustration created for “A Journey With Go”, made from the original Go Gopher, created by Renee French.
?? 這篇文章基于 Go 1.13 版本。
在 Go 中創(chuàng)建 gorotine 既方便又快捷,然而 Go 在同一時(shí)間內(nèi)最多在一個(gè)核上運(yùn)行一個(gè) gorotine,因此需要一種方法來存放其他的 gorotine,從而確保處理器(processor)負(fù)載均衡。
Goroutine 隊(duì)列
Go 使用兩級(jí)隊(duì)列來管理等待中的 goroutine,分別為本地隊(duì)列和全局隊(duì)列。每一個(gè)處理器都擁有本地隊(duì)列,而全局隊(duì)列是唯一的,且能被所有的處理器訪問到:
Global and local queues
每個(gè)本地隊(duì)列都有最大容量,為 256。在容量滿了之后,任意新到來的 Goroutine 都會(huì)被放置到全局隊(duì)列。下面的例子是,生產(chǎn)了上千個(gè) Goroutine 的程序:
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()
}
下面是擁有兩個(gè)處理器的調(diào)度器追蹤數(shù)據(jù)(traces):
Details of the local and global queues
追蹤數(shù)據(jù)通過 runqueue 展示了全局隊(duì)列中 Goroutine 的數(shù)量,以及方括號(hào)中 [3 256] 的本地隊(duì)列 goroutine 數(shù)量(分別為 P0 和 P1)。當(dāng)本地隊(duì)列滿了,積壓了 256 個(gè)等待中的 goroutine 后,下一個(gè) Goroutine 會(huì)被壓棧到全局隊(duì)列中,正如我們從 runqueue 看到的數(shù)量增長(zhǎng)一樣。
Goroutine 僅在本地隊(duì)列滿載之后才會(huì)加入到全局隊(duì)列;它也會(huì)在 Go 往調(diào)度器中批量注入時(shí)被加到全局隊(duì)列,例如,網(wǎng)絡(luò)輪詢器(network poller) 或者在垃圾回收期間等待的 goroutine。
下面是上一個(gè)例子的圖示:
Local queues have up to 256 goroutines
不過,我們還想知道,為什么本地隊(duì)列 P0 在上一個(gè)列子中不為空。因?yàn)?Go 使用了其他策略確保每個(gè)處理器都有任務(wù)處理。
任務(wù)竊取
如果處理器沒有任務(wù)可處理,它會(huì)按以下規(guī)則來執(zhí)行,直到滿足某一條規(guī)則:
從本地隊(duì)列獲取任務(wù) 從全局隊(duì)列獲取任務(wù) 從網(wǎng)絡(luò)輪詢器獲取任務(wù) 從其它的處理器的本地隊(duì)列竊取任務(wù)
在我們前面的例子中,主函數(shù)在 P1 上運(yùn)行并創(chuàng)建 goroutine。當(dāng)?shù)谝慌?gourinte 已經(jīng)進(jìn)入了 P1 的本地隊(duì)列時(shí),P0 正在尋找任務(wù)。然而,它的本地隊(duì)列,全局隊(duì)列,以及網(wǎng)絡(luò)輪詢器都是空的。最后的解決方法是從 P1 中竊取任務(wù)。
Work-stealing by P0
下面是調(diào)度器在發(fā)生任務(wù)竊取前后的追蹤數(shù)據(jù):

Work-stealing by P0
追蹤數(shù)據(jù)展示了,處理器是如何從其它處理器中竊取任務(wù)的。它從(其他處理器的)本地隊(duì)列中取走一半的 goroutine;在七個(gè) Goroutine 中,偷走了四個(gè) —— 其中一個(gè)立馬在 P0 執(zhí)行,剩下的放到本地隊(duì)列。現(xiàn)在處理器間工作處于負(fù)載良好的狀態(tài)。這能通過執(zhí)行 tracing 來確認(rèn):

goroutine 被合理地分發(fā),然后因?yàn)闆]有 I/O,goroutine 被鏈?zhǔn)綀?zhí)行而不需要切換。我們現(xiàn)在看一下,當(dāng)出現(xiàn)例如涉及到文件操作等 I/O 時(shí),會(huì)發(fā)生什么。
I/O 與全局隊(duì)列
一起看下涉及到文件操作的例子:
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()
}
變量 a 隨著時(shí)間以文件的字節(jié)數(shù)增加,下面是新的追蹤數(shù)據(jù):

在這個(gè)例子中,我們能看到每一個(gè) Goroutine 不只被一個(gè)處理器處理。在系統(tǒng)調(diào)用的情況下,當(dāng)調(diào)用完成后,Go 使用網(wǎng)絡(luò)輪詢器從全局隊(duì)列中把 gouroutine 取回來。這里是 Goroutine #35 的一個(gè)示意圖:
I/O operations put the work back to the global queue
當(dāng)一個(gè)處理器能從全局隊(duì)列中獲取任務(wù),第一個(gè)可用的處理器( P) 會(huì)執(zhí)行這個(gè) goroutine。這個(gè)行為解釋了,為什么一個(gè) Goroutine 能在不同的處理器中運(yùn)行,也展示了 Go 是如何讓空閑的處理器資源運(yùn)行 goroutine,從而進(jìn)行系統(tǒng)調(diào)用的優(yōu)化。
via: https://medium.com/a-journey-with-go/go-work-stealing-in-go-scheduler-d439231be64d
作者:Vincent Blanchon[1]譯者:LSivan[2]校對(duì):polaris1119[3]
本文由 GCTT[4] 原創(chuàng)編譯,Go 中文網(wǎng)[5] 榮譽(yù)推出
參考資料
Vincent Blanchon: https://medium.com/@blanchon.vincent
[2]LSivan: https://github.com/LSivan
[3]polaris1119: https://github.com/polaris1119
[4]GCTT: https://github.com/studygolang/GCTT
[5]Go 中文網(wǎng): https://studygolang.com/
推薦閱讀
站長(zhǎng) polarisxu
自己的原創(chuàng)文章
不限于 Go 技術(shù)
職場(chǎng)和創(chuàng)業(yè)經(jīng)驗(yàn)
Go語(yǔ)言中文網(wǎng)
每天為你
分享 Go 知識(shí)
Go愛好者值得關(guān)注
