對goland 中G、P、M的理解

Go調度器中的三種結構G、P、M

系統線程固定2M,且維護一堆上下文,對需求多變的併發應用並不友好,有可能造成內存浪費或內存不夠用。Go將併發的單位下降到線程以下,由其設計的goroutine初始空間非常小,僅2kb,但支持動態擴容到最大1G,這就是go自己的併發單元——goroutine協程。

實際上系統最小的執行單元仍然是線程,go運行時執行的協程也是掛載到某一系統線程之上的,這種協程與系統線程的調度分配由Go的併發調度器承擔,Go的併發調度器是屬於混合的二級調度併發模型,其內部設計有G、P、M三種抽象結構,我們來看一下它們分別是什麼:

G-P-M模型抽象結構:

  1. G: 表示Goroutine,每個Goroutine對應一個G結構體,G存儲Goroutine的運行堆棧、狀態以及任務函數,可重用。G運行隊列是一個棧結構,分全局隊列和P綁定的局部隊列,每個G不能獨立運行,它需要綁定到P才能被調度執行。
  2. P: Processor,表示邏輯處理器, 對G來說,P相當於CPU核,G只有綁定到P(在P的local runq中)才能被調度。對M來說,P提供了相關的執行環境(Context),如內存分配狀態(mcache),任務隊列(G)等,P的數量決定了系統內最大可並行的G的數量(前提:物理CPU核數 >= P的數量),P的數量由用戶設置的GOMAXPROCS決定,但是不論GOMAXPROCS設置爲多大,P的數量最大爲256。
  3. M: Machine,系統物理線程,代表着真正執行計算的資源,在綁定有效的P後,進入schedule循環;而schedule循環的機制大致是從Global隊列、P的Local隊列以及wait隊列中獲取G,切換到G的執行棧上並執行G的函數,調用goexit做清理工作並回到M,如此反覆。M並不保留G狀態,這是G可以跨M調度的基礎,M的數量是不定的,由Go Runtime調整,爲了防止創建過多OS線程導致系統調度不過來,目前默認最大限制爲10000個。

關於P這個設計,是在Go1.0之後才實現的,起初的Go併發性能並不十分亮眼,協程和系統線程的調度比較粗暴,導致很多性能問題,如全局資源鎖、M的內存過高等造成許多性能損耗,加入P的設計後實現了一個叫做 work-stealing 的調度算法:由P來維護Goroutine隊列並選擇一個適當的M綁定

G-P-M模型調度

我們來看看go關鍵字創建一個協程後其調度器是怎麼工作的:

  1. go關鍵字創建goroutine(G),優先加入某個P維護的局部隊列(當局部隊列已滿時才加入全局隊列);
  2. P需要持有或者綁定一個M,而M會啓動一個系統線程,不斷的從P的本地隊列取出G並執行;
  3. M執行完P維護的局部隊列後,它會嘗試從全局隊列尋找G,如果全局隊列爲空,則從其他的P維護的隊列裏竊取一般的G到自己的隊列;
  4. 重複以上知道所有的G執行完畢。

當然也有一些情況會造成Goroutine阻塞,如:

  1. 系統GC;
  2. 系統IO資源的調用,如文件讀寫;
  3. 網絡IO的延遲;
  4. 管道阻塞;
  5. 同步操作。

當遇到上述阻塞時,Go調度器也有相應的處理方式:

  1. 1.系統調度引起阻塞:

如系統GC,M會解綁P,出讓控制權給其他M,讓該P維護的G運行隊列不至於阻塞。

  1. 2.用戶態的阻塞:

當goroutine因爲管道操作或者系統IO、網絡IO而阻塞時,對應的G會被放置到某個等待隊列,該G的狀態由運行時變爲等待狀態,而M會跳過該G嘗試獲取並執行下一個G,如果此時沒有可運行的G供M運行,那麼M將解綁P,並進入休眠狀態;當阻塞的G被另一端的G2喚醒時,如管道通知,G又被標記爲可運行狀態,嘗試加入G2所在P局部隊列的隊頭,然後再是G全局隊列。

  1. 3.當存在空閒的P時,竊取其他隊列的G:

當P維護的局部隊列全部運行完畢,它會嘗試在全局隊列獲取G,直到全局隊列爲空,再向其他局部隊列竊取一般的G。

至此Go的調度器模型解析完畢。基於Go調度器的優越設計,它號稱能實現百萬級併發,即使日常很難達到這種併發量,我們也應該對併發的使用要心存敬畏,真正的併發依賴於物理核心,啓動併發是需要系統開銷的,雖然在Go的運行時它看起來很小,但量變引起質變,當業務啓動的併發到十萬級、百萬級甚至千萬級時,其性能開銷還是非常巨大的。可以通過一定的手段控制併發數量以防止系統奔潰,如實現一個協程池,通過worker機制控制併發數。

Ok,希望學完這一專題你會對Go的併發有更深刻的瞭解。

發佈了331 篇原創文章 · 獲贊 132 · 訪問量 75萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章