Go goroutine調度 頂 原 薦

前言

Goroutine調度是一個很複雜的機制,儘管Go源碼中提供了大量的註釋,但對其原理沒有一個好的理解的情況下去讀源碼收穫不會很大。下面嘗試用簡單的語言描述一下Goroutine調度機制,在此基礎上再去研讀源碼效果可能更好一些。

1. 線程池的缺陷

我們知道,在高併發應用中頻繁創建線程會造成不必要的開銷,所以有了線程池。線程池中預先保存一定數量的線程,而新任務將不再以創建線程的方式去執行,而是將任務發佈到任務隊列,線程池中的線程不斷的從任務隊列中取出任務並執行,可以有效的減少線程創建和銷燬所帶來的開銷。

下圖展示一個典型的線程池:

爲了方便下面的敘述,我們把任務隊列中的每一個任務稱作G,而G往往代表一個函數。
線程池中的線程worker線程不斷的從任務隊列中取出任務並執行。而worker線程的調度則交給操作系統進行調度。

如果worker線程執行的G任務中發生系統調用,則操作系統會將該線程置爲阻塞狀態,也意味着該線程在怠工,也意味着消費任務隊列的worker線程變少了,也就是說線程池消費任務隊列的能力變弱了。

如果任務隊列中的大部分任務都會進行系統調用,則會讓這種狀態惡化,大部分worker線程進入阻塞狀態,從而任務隊列中的任務產生堆積。

解決這個問題的一個思路就是重新審視線程池中線程的數量,增加線程池中線程數量可以一定程度上提高消費能力,但隨着線程數量增多,由於過多線程爭搶CPU,消費能力會有上限,甚至出現消費能力下降。 如下圖所示:

2. Goroutine調度器

線程數過多,意味着操作系統會不斷的切換線程,頻繁的上下文切換就成了性能瓶頸。 Go提供一種機制,可以在線程中自己實現調度,上下文切換更輕量,從而達到了線程數少,而併發數並不少的效果。而線程中調度的就是Goroutine.

早期Go版本,比如1.9.2版本的源碼註釋中有關於調度器的解釋。
Goroutine 調度器的工作就是把“ready-to-run”的goroutine分發到線程中。

Goroutine主要概念如下:

  • G(Goroutine): 即Go協程,每個go關鍵字都會創建一個協程。
  • M(Machine): 工作線程,在Go中稱爲Machine。
  • P(Processor): 處理器(Go中定義的一個摡念,不是指CPU),包含運行Go代碼的必要資源,也有調度goroutine的能力。

M必須擁有P纔可以執行G中的代碼,P含有一個包含多個G的隊列,P可以調度G交由M執行。其關係如下圖所示:

圖中M是交給操作系統調度的線程,M持有一個P,P將G調度進M中執行。P同時還維護着一個包含G的隊列(圖中灰色部分),可以按照一定的策略將不能的G調度進M中執行。

P的個數在程序啓動時決定,默認情況下等同於CPU的核數,由於M必須持有一個P纔可以運行Go代碼,所以同時運行的M個數,也即線程數一般等同於CPU的個數,以達到儘可能的使用CPU而又不至於產生過多的線程切換開銷。

程序中可以使用runtime.GOMAXPROCS()設置P的個數,在某些IO密集型的場景下可以在一定程度上提高性能。這個後面再詳細介紹。

3. Goroutine調度策略

3.1 隊列輪轉

上圖中可見每個P維護着一個包含G的隊列,不考慮G進入系統調用或IO操作的情況下,P週期性的將G調度到M中執行,執行一小段時間,將上下文保存下來,然後將G放到隊列尾部,然後從隊列中重新取出一個G進行調度。

除了每個P維護的G隊列以外,還有一個全局的隊列,每個P會週期性的查看全局隊列中是否有G待運行並將期調度到M中執行,全局隊列中G的來源,主要有從系統調用中恢復的G。之所以P會週期性的查看全局隊列,也是爲了防止全局隊列中的G被餓死。

3.2 系統調用

上面說到P的個數默認等於CPU核數,每個M必須持有一個P纔可以執行G,一般情況下M的個數會略大於P的個數,這多出來的M將會在G產生系統調用時發揮作用。類似線程池,Go也提供一個M的池子,需要時從池子中獲取,用完放回池子,不夠用時就再創建一個。

當M運行的某個G產生系統調用時,如下圖所示:

如圖所示,當G0即將進入系統調用時,M0將釋放P,進而某個空閒的M1獲取P,繼續執行P隊列中剩下的G。而M0由於陷入系統調用而進被阻塞,M1接替M0的工作,只要P不空閒,就可以保證充分利用CPU。

M1的來源有可能是M的緩存池,也可能是新建的。當G0系統調用結束後,跟據M0是否能獲取到P,將會將G0做不同的處理:

  1. 如果有空閒的P,則獲取一個P,繼續執行G0。
  2. 如果沒有空閒的P,則將G0放入全局隊列,等待被其他的P調度。然後M0將進入緩存池睡眠。

3.3 工作量竊取

多個P中維護的G隊列有可能是不均衡的,比如下圖:

豎線左側中右邊的P已經將G全部執行完,然後去查詢全局隊列,全局隊列中也沒有G,而另一個M中除了正在運行的G外,隊列中還有3個G待運行。此時,空閒的P會將其他P中的G偷取一部分過來,一般每次偷取一半。偷取完如右圖所示。

4. GOMAXPROCS設置對性能的影響

一般來講,程序運行時就將GOMAXPROCS大小設置爲CPU核數,可讓Go程序充分利用CPU。
在某些IO密集型的應用裏,這個值可能並不意味着性能最好。 理論上當某個Goroutine進入系統調用時,會有一個新的M被啓用或創建,繼續佔滿CPU。 但由於Go調度器檢測到M被阻塞是有一定延遲的,也即舊的M被阻塞和新的M得到運行之間是有一定間隔的,所以在IO密集型應用中不妨把GOMAXPROCS設置的大一些,或許會有好的效果。

5.參考文章

5.1 《The Go scheduler》http://morsmachine.dk/go-scheduler

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章