1. 程式人生 > 其它 >【轉載】 圖解協程排程模型-GMP模型

【轉載】 圖解協程排程模型-GMP模型

現在無論是客戶端、服務端或web開發都會涉及到多執行緒的概念。那麼大家也知道,執行緒是作業系統能夠進行運算排程的最小單位,同一個程序中的多個執行緒都共享這個程序的全部系統資源。

執行緒

三個基本概念

  • 核心執行緒:在核心空間實現的執行緒,由核心管理
  • 使用者執行緒:在使用者空間實現的執行緒,不歸核心管理,由使用者態完成管理
  • 輕量級程序(LWP):在核心中支援使用者執行緒(使用者執行緒與核心執行緒的中間層,核心執行緒的高度抽象)

執行緒模型

1. 一對一模型(核心級執行緒模型)

由程序建立LWP,由於每個LWP對應一個核心級執行緒,所以使用者也就是在建立一個一個核心執行緒,由核心管理執行緒。我們熟知的大多數語言就是屬於一對一模型,比如C#、java、c等(這些語言也有相應的方式實現使用者態執行緒,只是語言本身的執行緒是一對一模型)

2. 一對多模型(使用者級模型)

使用者程序建立多個使用者級執行緒對應在同一個LWP上,所以本質上在核心態只會有一個核心執行緒執行,那麼使用者及執行緒的排程就是在使用者態通過使用者空間的執行緒庫對執行緒進行管理。

這類模型優點就是排程在使用者態,所以不用頻繁地進行核心態與使用者態切換,大大提升執行緒效率。python語言的協程就是這種模型實現的,但是這種模型並不能實現真正意義上的的併發。

3. 多對多模型(使用者級與核心級執行緒模型)

多對多模型也就是在上述兩個模型基礎上做一些優點的集合。使用者態的多個執行緒對應多個LWP,但它們之間不是一一對應的,在使用者態管理排程每個LWP對應的使用者執行緒繫結關係。

所以多對多模型是可以有更多的使用者態執行緒在相對於比較少的核心執行緒上執行的。這種使用者態執行緒也就是我們平時說的協程。Go語言的協程就是多對多模型,它由Go語言的runtime在使用者態做排程,這也是Go語言高併發的原因。

協程(Goroutine)

前面我們說到,協程就是使用者態執行緒,它的所有排程都在使用者態實現,協程這方面Go語言應該是最具代表性的(畢竟以高併發為賣點),以Go語言的協程--Goroutine作為研究物件。

GM模型

在Go語言設計的初期,Go團隊使用簡單的GM模型實現協程。

G:goroutine,也就是協程,它一般所佔的棧記憶體為2KB,執行過程中如果棧空間不夠用則會自動擴容。它可以被理解成一個被打包的程式碼段。goroutine並不是一個執行單元。

M:machine工作執行緒,就是真正用來執行程式碼的執行緒。由Go的runtime管理好它的生命週期。

在Go語言初期使用一個全域性的佇列來儲存所有的G。當用戶新建一個G的時候,runtime就會將打包好的G放到全域性佇列的隊尾,並維護好隊尾指標。當M執行完一個G後,就會到全域性佇列的隊頭取一個G給M執行,並且維護好全域性佇列的隊頭指標。

可以發現這裡有個很嚴重的問題,如果放任隨意加入G,也放任任何M隨意取G,那麼就會出現併發問題。解決併發問題很簡單,就是加鎖,Go語言團隊也確實是這樣做的。那麼加了鎖之後也就代表所有的存取操作都會涉及到這個單一的全域性互斥鎖,那整個排程的執行效率大打折扣。除了鎖導致效能問題以外,還有類似系統呼叫時,工作執行緒經常被阻塞和取消阻塞,等等一些問題[2]。所以後續官方團隊調整了排程模型,加入了P的概念。

GMP模型

新的模型中引入了P的概念,G與M沒有什麼大的變化。

P:process處理器,代表了M所需的上下文環境,也可以理解為一個M執行所需要的Token,當P有任務時需要建立或者喚醒一個M來執行它佇列裡的任務。所以P的數量決定了並行執行任務的數量,可以通過runtime.GOMAXPROCS來設定,現在的Go版本中預設為CPU的核心數。

上圖根據曹大的GMP模型圖自行簡化繪製的。

P結構

一個P對應一個M,P結構中帶有runnext欄位和一個本地佇列,runnext欄位中儲存的就是下一個被M執行的G,每個P帶有一個256大小的本地陣列佇列,這個佇列是無鎖的,這兩個資料結構類似於快取的概念,可以有效的解決全域性佇列被頻繁訪問的問題,而且runnext和本地佇列是PM結構本地的,沒有其他的執行單元競爭,所以也不用加鎖。

本部落格是博主個人學習時的一些記錄,不保證是為原創,個別文章加入了轉載的源地址還有個別文章是彙總網上多份資料所成,在這之中也必有疏漏未加標註者,如有侵權請與博主聯絡。