通俗易懂的Go协程的引入及GMP模型简介
本文根據Golang深入理解GPM模型加之自己的理解整理而來
Go協程的引入及GMP模型
- 一、協程的由來
- 1. 單進程操作系統
- 2. 多線程/多進程操作系統
- 3. 引入協程
- 二、golang對協程的處理
- 1. 對co-routine對處理
- 2. 對調度器的優化(早期)
- 三、goroutine調度器的GMP模型
- 1. gmp模型簡介
- 2. gmp模型調度器設計策略
- 1. 復用線程
- 2. 利用并行
- 3. 搶占
- 4. 全局g隊列
一、協程的由來
1. 單進程操作系統
早期的單進程操作系統每個進程/線程(cpu無法區別線程還是進程)都是順序執行的,
帶來了兩個問題:
- 單一執行流程,計算機只能一個任務一個任務的進行處理
- 進程阻塞帶來的cpu時間浪費
2. 多線程/多進程操作系統
針對以上不足,引出了多線程/多進程操作系統
這種方式解決了多線程/多進程間的阻塞問題,但該方式又引入了新的問題,就是切換成本問題,從一個線程切換到下一個線程的時候需要保存當前線程的狀態,會涉及各種系統調用、上下文切換、拷貝復制,這些東西我們稱之為cpu浪費時間成本。
這樣會導致,如果進程/線程的數量過多的情況下,切換成本就更大,cpu利用率大大降低,一個cpu看起來可能滿負載,但其實其中只有%50用來執行真正的程序,剩余時間都在進行頻繁的線程切換。
不僅如此,多線程還往往伴隨著同步競爭的問題,因此開發設計越來越復雜,在實際運行中,為了達到更好的并發效果,我們往往給單獨的任務分配一個線程進行執行,當任務越來越多的情況下,不僅會造成cpu高消耗調度的問題,還會出現高內存占用的問題,在一個32的操作系統中,往往一個進程會占用4g的虛擬內存,一個線程會占用4m左右內存。
因此 提高cpu利用率、減小內存消耗 才是當今需要解決的事情,那么怎么優化呢?
3. 引入協程
一個線程分為內核態和用戶態兩個部分,于是工程師們想著將這兩部分分開,將一個線程分為用戶線程、內核線程兩部分,兩者之間進行綁定,這樣就可以各司其職,內核線程專門用來與硬件底層進行一些交互,而用戶線程用來保證業務層面的并發效果,而且這樣cpu的視野就只有內核線程,只與內核線程進行交互
將一個線程分開兩部分以后,我們將其中的用戶空間的部分稱之為協程,將內核空間部分稱之為線程
在以上基礎上,可以進行進一步優化, 我們可以讓一個(內核)線程通過一個協程調度器來綁定多個協程,這樣cpu調度時無感,還是只針對(內核)線程,但是上層開了多個協程后,可以讓每一個協程掛載一個任務,這樣在用戶態能夠保證并發效果,且由于cpu只針對內核空間的線程,多個協程之間cpu是不需要切換的,這樣就解決了前面高消耗cpu的瓶頸
也就形成了上圖一個線程對應多個協程的關系,當今計算機都是多核的,因此線程和協程之間往往采用以下多對多的方式,每個cpu針對一個線程來綁定多個協程,這樣可以達到更好的并發效果
由于內核空間的cpu對線程的調度無法干涉,因此優化的主要目標移動到了用戶空間里對協程調度器的優化,如果對其進行優化能夠達到更高的并發性。
二、golang對協程的處理
1. 對co-routine對處理
golang在對協程調度器處理之前,首先對co-routine協程進行了處理,首先將其改名字為goroutine,其次修改了goroutine的所占的內存大小,砍掉了多余不必要的空間,使得每個goroutin所占內存大小為幾kb,實現了可以存在大量的goroutine,解決了高內存消耗的問題。
2. 對調度器的優化(早期)
然后便對協程調度器進行了優化,實現了靈活調度
golang早期調度器形式如下圖所示,協程與線程之間是多對多的關系,其中維護了一個全局的goroutine隊列,每創建一個協程,都將起放到這個隊列中。當其中的線程要調度協程時,首先會獲取全局隊列的鎖,然后嘗試去執行goroutine,隊列中剩余goroutine向隊列頭移動,當執行完后,將 執行完的goroutine放回隊列尾部。
早期的調度器簡單,但是存在很多的弊端:
- 創建、銷毀、調度G都需要每個M獲取鎖,這就形成了激烈的鎖競爭。
- M轉移G會造成延遲和額外的系統負載。
- 系統調用(CPU在M之間的切換)導致頻繁的線程阻塞和取消阻塞操作增加了系統開銷。
三、goroutine調度器的GMP模型
1. gmp模型簡介
G指的是goroutine,協程,也就是上述一個線程分半后用戶狀態下的線程P指的是processor,用于處理執行goroutine,包含了每一個goroutine所需要的上下文環境。每個P維護了一個本地隊列,存放當前P即將要執行的goroutine,此外還有一個全局隊列,用于存放等待運行的goroutine。每個P的本地隊列有數量限制,一般不超過256G,新建一個goroutine的時候,優先放到P的本地隊列中,如果隊列滿了,才會嘗試放到全局隊列中。P的數量可以通過GOMAXPROCS()來設置,它代表了真正的并發度,即有多少個goroutine可以同時運行M指的是Machine,物理線程,也就是上述一個線程分半后內核狀態下的線程
GMP模型的整個流程圖如下所示,由全局隊列、P的本地隊列、P列表、M列表幾個部分組成:
其中P列表是在程序啟動時創建的,其數量最多有GOMAXPROCS個,有兩種配置方式:
- 通過配置環境變量$GOMAXPROCS
- 在程序中通過runtime.GOMAXPROCS()方法來設置
M列表的數量表示當前操作系統分配給當前go程序的內核線程數,與P的數量無關,go語言本身限定了M的最大量為10000個,我們一般不對其數量進行設置,因為其數量是動態變化的,因為有一個M阻塞就會創建一個新的M,如果有M空閑,就會對其回收或者睡眠;如果需要對其數量進行設置,可以通過runtime/debug包下的SetMaxThreads函數進行設置
gmp模型的調度過程可以理解為,當一個任務需要執行時,首先由cpu調度分配一個線程M,然后M會獲取該線程的P,P從本地隊列/全局隊列中取出一個G進行執行,也就是同一時間一個P只能執行一個G,因此一個程序當前所能執行最高的G的數量就是P的數量,也就是GOMAXPROCS個
2. gmp模型調度器設計策略
gmp模型對調度器的優化主要集中在以下幾個策略:
- 復用線程
- 利用并行
- 搶占
- 全局G隊列
1. 復用線程
為了避免頻繁的創建、銷毀線程,而是對線程進行復用,gmp模型調度器采用了兩種機制work stealing 和 hand off
1?? work stealing
假設M1與P1綁定正在執行G1協程,當前P1的本地隊列中還有G2、G3等待被執行,但此時M2對應的P2空閑,work stealing機制就是M2想要執行協程的話就從M1的P1的本地隊列中偷取G進行執行
2?? hand off
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-xTaE4Wus-1632563686593)(https://gitee.com/zhong_siru/images/raw/master//img/202109251753330.png)]
此時假設M1的P1正在執行G1,M2的P2即將執行G3,但此時G1阻塞,hand off機制就是將M1與其P1分離開,此時cpu會創建一個新的線程,然后將P1遷移到新創建的線程M3中,cpu調度執行M2和M3,相當于M3接管了M1之前綁定P繼續執行。此時原來的M1和G1處與阻塞狀態,如果G1的阻塞操作執行完畢后,還需要執行的話就會加入到其他隊列中,如果不需要執行則直接被銷毀。
2. 利用并行
該策略就是利用GOMAXPROCS來限定P的個數,例如設置為 CPU核數/2,這樣該程序跑起來最多用到一半的cpu,其他的cpu給其他程序使用
3. 搶占
以前的co-routine綁定一個cpu的時候,如果此時來了其他的co-routine,只有當前co-routine結束主動釋放時該cpu才會給其他co-routine進行綁定
而現在goroutine綁定一個cpu的時候,如果有其他的goroutine等待運行,則當前g最多執行10ms,10ms一到不管當前g是否主動釋放,當前在等待的g一定會搶占cpu,這樣保證了每個g都是平等的,防止饑餓現象
4. 全局g隊列
如果一個P的本地隊列里已經沒有G待執行的話,會優先從其他P的本地隊列里面偷,如果都沒有的話才會從全局隊列里面取,取出與放回的過程涉及全局隊列的加鎖與鎖釋放
總結
以上是生活随笔為你收集整理的通俗易懂的Go协程的引入及GMP模型简介的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 操作系统学习笔记 第六章:设备管理(王道
- 下一篇: Jenkins首次安装推荐插件出错 No