### go協程調度
### 核心圖




### G-P-M 模型調度
Go調度器工作時會維護兩種用來保存G的任務隊列:一種是一個Global任務隊列,一種是每個P維護的Local任務隊列。
當通過go關鍵字創建一個新的goroutine的時候,它會優先被放入P的本地隊列。為了運行goroutine,M需要持有(綁定)一個P,接著M會啟動一個OS線程,循環從P的本地隊列里取出一個goroutine并執行。當然還有上文提及的 work-stealing調度算法:當M執行完了當前P的Local隊列里的所有G后,P也不會就這么在那躺尸啥都不干,它會先嘗試從Global隊列尋找G來執行,如果Global隊列為空,它會隨機挑選另外一個P,從它的隊列里中拿走一半的G到自己的隊列中執行。
如果一切正常,調度器會以上述的那種方式順暢地運行,但這個世界沒這么美好,總有意外發生,以下分析goroutine在兩種例外情況下的行為。
Go runtime會在下面的goroutine被阻塞的情況下運行另外一個goroutine:
- blocking syscall (for example opening a file)
- network input
- channel operations
- primitives in the sync package
這四種場景又可歸類為兩種類型:
### 用戶態阻塞/喚醒
當goroutine因為channel操作或者network I/O而阻塞時(實際上golang已經用netpoller實現了goroutine網絡I/O阻塞不會導致M被阻塞,僅阻塞G,這里僅僅是舉個栗子),對應的G會被放置到某個wait隊列(如channel的waitq),該G的狀態由_Gruning變為_Gwaitting,而M會跳過該G嘗試獲取并執行下一個G,如果此時沒有runnable的G供M運行,那么M將解綁P,并進入sleep狀態;當阻塞的G被另一端的G2喚醒時(比如channel的可讀/寫通知),G被標記為runnable,嘗試加入G2所在P的runnext,然后再是P的Local隊列和Global隊列。
### 系統調用阻塞
當G被阻塞在某個系統調用上時,此時G會阻塞在_Gsyscall狀態,M也處于 block on syscall 狀態,此時的M可被搶占調度(可以搶占其他M):執行該G的M會與P解綁,而P則嘗試與其它idle的M綁定,繼續執行其它G。如果沒有其它idle的M,但P的Local隊列中仍然有G需要執行,則創建一個新的M;當系統調用完成后,G會重新嘗試獲取一個idle的P進入它的Local隊列恢復執行,如果沒有idle的P,G會被標記為runnable加入到Global隊列。(全局隊列用武之地)
以上就是從宏觀的角度對Goroutine和它的調度器進行的一些概要性的介紹,當然,Go的調度中更復雜的搶占式調度、阻塞調度的更多細節,大家可以自行去找相關資料深入理解,本文只講到Go調度器的基本調度過程,為后面自己實現一個Goroutine Pool提供理論基礎,這里便不再繼續深入上述說的那幾個調度了,事實上如果要完全講清楚Go調度器,一篇文章的篇幅也實在是捉襟見肘,所以想了解更多細節的同學可以去看看Go調度器 G-P-M 模型的設計者 Dmitry Vyukov 寫的該模型的設計文檔《Go Preemptive Scheduler Design》以及直接去看源碼,G-P-M模型的定義放在src/runtime/runtime2.go里面,而調度過程則放在了src/runtime/proc.go里。
### 問題?
#### 0.go協程阻塞時如何進行調度?
> 在程序中任何對系統 API 的調用,都會被 runtime 層攔截來方便調度。
> Goroutine 在 system call 和 channel call 時都可能發生阻塞,但這兩種阻塞發生后,處理方式又不一樣的。
> 1.當程序發生 system call,M 會發生阻塞,同時喚起(或創建)一個新的 M 繼續執行其他的 G。當MO返回時,它必須嘗試取得一個context P來運行goroutine,一般情況下,它會從其他的OS線程那里steal偷一個context過來,如果沒有偷到的話,它就把goroutine放在一個global runqueue里,然后自己就去睡大覺了(放入線程緩存里)。Contexts們也會周期性的檢查global runqueue,否則global runqueue上的goroutine永遠無法執行。
> 2.當程序發起一個 channel call,程序可能會阻塞,但不會阻塞 M,G 的狀態會設置為 waiting,M 繼續執行其他的 G。當 G 的調用完成,會有一個可用的 M 繼續執行它。
#### 1.go為什么要實現自己的協程調度,而不用系統調度?
> 1.線程較多時,開銷較大。
> 2.OS 的調度,程序不可控。而 Go GC 需要停止所有的線程,使內存達到一致狀態。
#### 2.GM為啥不行?P有什么作用?
> 1.每個 P 都有一個隊列,用來存正在執行的 G。避免 Global Sched Lock。
> 2.每個 M 運行都需要一個 MCache 結構。M Pool 中通常有較多 M,但執行的只有幾個,為每個池子中的每個 M 分配一個 MCache 則會形成不必要的浪費,通過把 cache 從 M 移到 P,每個運行的 M 都有關聯的 P,這樣只有運行的 M 才有自己的 MCache。
#### 3.Goroutine vs OS thread 有什么區別?
> 其實 goroutine 用到的就是線程池的技術,當 goroutine 需要執行時,會從 thread pool 中選出一個可用的 M 或者新建一個 M。而 thread pool 中如何選取線程,擴建線程,回收線程,Go Scheduler 進行了封裝,對程序透明,只管調用就行,從而簡化了 thread pool 的使用。
#### 4.sysmon功能是什么?
>釋放閑置超過5分鐘的span物理內存;
>如果超過2分鐘沒有垃圾回收,強制執行;
>將長時間未處理的netpoll結果添加到任務隊列;
>向長時間運行的G任務發出搶占調度;
>收回因syscall長時間阻塞的P;