Go 語言調度器

Go 語言的運行時管理了調度,垃圾回收,和協程的運行時環境。這裏我們只討論調度器。

運行時調度器將協程映射到系統線程上執行。協程是輕量級的線程,啓動協程的開銷十分小。每個協程被一個稱爲 G 的結構體所描述,它包含了記錄協程棧和當前狀態的必要字段。所以,G = 協程

運行時保持跟蹤每個 G 並將它們映射到邏輯處理器上,邏輯處理器稱爲 P。P 可以被看成一個抽象資源或者一個上下文(context),它需要被系統線程(稱爲 M,或者 Machine)獲取,然後系統線程纔可以執行 G

你可以通過在運行時調用runtime.GOMAXPROCS(numLogicalProcessors)來控制邏輯處理器的數量,如果你真打算調整這個參數(也許你不應該調整它),最好只設置一次,因爲這個調用會引起垃圾回收器 STW(即程序暫停執行)。

基本來說,操作系統運行線程,線程運行你的代碼。Go 的把戲是編譯器在運行時的一些位置插入一些代碼(比如通過 channel 發送數據,調用運行時包中的函數等),這樣 Go 才能通知調度器做相應的處理。

注:上圖來源於 Analysis of the Go runtime scheduler

M P G 間的交互

M P G 間的交互有一點點複雜。先看下面這張來自 go runtime scheduler slides by Gao Chao 的流程圖,畫得非常好。

圖中可以看到,有兩種類型的 G 隊列:一個全局隊列在 schedt 結構體中(很少被使用),另外每個 P 持有一個可運行 G 的隊列。

爲了運行一個協程,M 需要持有上下文 P。然後 M 從所持有的 P 中的協程隊列中取出協程並執行協程中的代碼。

當你生成一個新協程時(調用go func()),這個協程被放入 P 的隊列中。這裏有一個有趣的偷取調度算法,當 M 執行完了一些 G 後試圖再次從隊列中獲取 G,而此時隊列爲空,那麼 M 會隨機選取另一個 P 並試圖從選取的 P偷取一半數量的可執行 G

還有一件有趣的事情是,當你在協程中調用一個阻塞式的系統調用。阻塞式的系統調用會被攔截,如果這個 P 上還有其它的 G 等待被運行,Go 的運行時會將這個 P 的系統線程分離出來,然後再創建一個新的系統線程(如果沒有空閒線程的話)來爲這個 P 服務。

譯者 yoko 注
這裏說的攔截系統調用,就是文章開頭所說的 Go 編譯器在真正的系統調用前後插入了代碼。
阻塞的協程在阻塞前帶着系統線程跑了(和所屬的 P 說拜拜),新生成一個系統線程和 P 掛載,繼續運行 P 上後續的協程。

當這個系統調用完成時,系統調用所屬的協程會被放回一個本地運行隊列,而之前運行這個系統調用的系統線程會被標識爲非運行狀態並將這個線程插入空閒線程列表中。

譯者 yoko 注
使得調用完系統調用的協程可以繼續被正常調度,系統調用之後的代碼被正常執行。
系統線程則被回收,交給 Go 調度器管理。

如果某個協程發起了網絡調用,運行時也會做相似的處理。調用會被攔截,但是由於 Go 集成了 network poller,它有自己獨立的系統線程,所以這個協程會被指派過去。

基本來說,如果當前協程阻塞在以下幾種情況時,Go 的運行時會運行另一個協程。

跟蹤調度器

Go 允許跟蹤打印運行時調度信息。方法是設置 GODEBUG 環境變量:

GODEBUG=scheddetail=1,schedtrace=1000 ./program

輸出示例:

SCHED 0ms: gomaxprocs=idleprocs=threads=spinningthreads=idlethreads=runqueue=gcwaiting=nmidlelocked=stopwait=sysmonwait=0
  P0: status=schedtick=syscalltick=m=runqsize=gfreecnt=0
  P1: status=schedtick=syscalltick=m=-1 runqsize=gfreecnt=0
  P2: status=schedtick=syscalltick=m=-1 runqsize=gfreecnt=0
  P3: status=schedtick=syscalltick=m=-1 runqsize=gfreecnt=0
  P4: status=schedtick=syscalltick=m=-1 runqsize=gfreecnt=0
  P5: status=schedtick=syscalltick=m=-1 runqsize=gfreecnt=0
  P6: status=schedtick=syscalltick=m=-1 runqsize=gfreecnt=0
  P7: status=schedtick=syscalltick=m=-1 runqsize=gfreecnt=0
  M1: p=-1 curg=-1 mallocing=throwing=preemptoff= locks=dying=helpgc=spinning=false blocked=false lockedg=-1
  M0: p=curg=mallocing=throwing=preemptoff= locks=dying=helpgc=spinning=false blocked=false lockedg=1
  G1: status=8() m=lockedm=0

一般來說,你不需要那麼詳細的信息,所以你可以這樣:

GODEBUG=schedtrace=1000 ./program

這裏有一篇 William Kennedy 寫的非常棒的文章 (https://www.goinggo.net/2015/02/scheduler-tracing-in-go.html),詳細描述瞭如何使用 trace。

另外,還有一個值得推薦的工具叫做go tool trace,它有 UI 界面,用於展示你的程序以及運行時做了什麼。你可以看這篇文章 (https://making.pusher.com/go-tool-trace) 學習如何使用它。

轉自:

pengrl.com/p/22729/

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/O2oEtGRD4yddvHmbLUzs5A