歡迎訪問我的個人網站獲取更佳排版體驗:pengrl.com/p/22729/

基礎

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的運行時會運行另一個協程。

  • 阻塞式的系統調用(比如打開一個文件)
  • 網路IO
  • channel操作
  • sync包中的同步原語

跟蹤調度器

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

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

輸出示例:

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

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

$ GODEBUG=schedtrace=1000 ./program

這裡有一篇William Kennedy寫的非常棒的文章,詳細描述了如何使用trace。

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

引用鏈接:

  • Slides by Matthew Dale
  • Columbia University paper: Analysis of the Go runtime scheduler
  • Scalable Go Scheduler Design Doc
  • Hacker news chat which explains a lot
  • go runtime scheduler slides by Gao Chao
  • Morsmachine article

英文原文:povilasv.me/go-schedule

推薦閱讀:

相关文章