Golang 調度器 GPM模型

Golang 調度器 GPM模型

1 多進程/線程時代有了調度器需求

在多進程/多線程的操作系統中,就解決了阻塞的問題,因為一個進程阻塞cpu可以立刻切換到其他進程中去執行,而且調度cpu的算法可以保證在運行的進程都可以被分配到cpu的運行時間片。這樣從宏觀來看,似乎多個進程是在同時被運行。

在這里插入圖片描述

上圖為一個CPU通過調度器切換CPU時間軸的情景。如果未來滿足宏觀上每個進程/線程是一起執行的,則CPU必須切換,每個進程會被分配到一個時間片中。

但新的問題就又出現了,進程擁有太多的資源,進程的創建、切換、銷毀,都會占用很長的時間,CPU雖然利用起來了,但如果進程過多,CPU有很大的一部分都被用來進行進程調度了。如下圖所示:

在這里插入圖片描述

對于Linux操作系統來言,CPU對進程和線程的態度是一樣的,如圖1.3所示,如果系統的CPU數量過少,而進程/線程數量比較龐大,則相互切換的頻率也就會很高,其中中間的切換成本越來越大。這一部分的性能消耗實際上是沒有做在對程序有用的計算算力上,所以盡管線程看起來很美好,但實際上多線程開發設計會變得更加復雜,開發者要考慮很多同步競爭的問題,如鎖、資源競爭、同步沖突等。

2 協程來提高CPU利用率

那么如何才能提高CPU的利用率呢?多進程、多線程已經提高了系統的并發能力,但是在當今互聯網高并發場景下,為每個任務都創建一個線程是不現實的,因為這樣就會出現極大量的線程同時運行,不僅切換頻率高,也會消耗大量的內存:進程虛擬內存會占用4GB(32位操作系統),而線程也要大約4MB。大量的進程或線程出現了以下兩個新的問題。

  • (1)高內存占用。
  • (2)調度的高消耗CPU。

工程師發現其實可以把一個線程分為“內核態”和“用戶態”兩種形態的線程。所謂用戶態線程就是把內核態的線程在用戶態實現了一遍而已,目的是更輕量化(更少的內存占用、更少的隔離、更快的調度)和更高的可控性(可以自己控制調度器)。用戶態中的所有東西內核態都看得見,只是對于內核而言用戶態線程只是一堆內存數據而已。

一個用戶態線程必須綁定一個內核態線程,但是CPU并不知道有用戶態線程的存在,它只知道它運行的是一個內核態線程(Linux的PCB進程控制塊),如下圖所示:

在這里插入圖片描述

如果將線程再進行細化,內核線程依然叫 線程(Thread) ,而用戶線程則叫 協程(Co-routine) 。操作系統層面的線程就是所謂的內核態線程,用戶態線程則多種多樣,只要能滿足在同一個內核線程上執行多個任務,例如Co-routine、Go的Goroutine、C#的Task等。

既然一個協程可以綁定一個線程,那么能不能多個協程綁定一個或者多個線程呢?接下來有3種協程和線程的映射關系,它們分別是 N : 1 關系、 1 : 1 關系和 M : N 關系。

3 N比1關系

N個協程綁定1個線程,優點就是協程在用戶態線程即完成切換,不會陷入內核態,這種切換非常輕量快速,但缺點也很明顯,1個進程的所有協程都綁定在1個線程上,如圖所示。

在這里插入圖片描述

N:1關系面臨的幾個問題如下:

  • (1) 某個程序用不了硬件的多核加速能力。
  • (2) 某一個協程阻塞,會造成線程阻塞,本進程的其他協程都無法執行了,進而導致沒有任何并發能力。

4 1比1關系

1個協程綁定1個線程,這種方式最容易實現。協程的調度都由CPU完成了,雖然不存在N:1的缺點,但是協程的創建、刪除和切換的代價都由CPU完成,成本和代價略顯昂貴。協程和線程的1:1關系如圖所示。

在這里插入圖片描述

5 M比N關系

M個協程綁定1個線程,是 N: 11 : 1 類型的結合,克服了以上兩種模型的缺點,但實現起來最為復雜。同一個調度器上掛載M個協程,調度器下游則是多個CPU核心資源。協程跟線程是有區別的,線程由CPU調度是搶占式的,協程由用戶態調度是協作式的,一個協程讓出CPU后,才執行下一個協程,所以針對 M : N 模型的中間層的調度器設計就變得尤為重要,提高線程和協程的綁定關系和執行效率也變為不同語言在設計調度器時的優先目標。

在這里插入圖片描述

6 Go語言的協程goroutine

Go語言為了提供更容易使用的并發方法,使用了Goroutine和Channel。Goroutine來自協程的概念,讓一組可復用的函數運行在一組線程之上,即使有協程阻塞,該線程的其他協程也可以被runtime調度,從而轉移到其他可運行的線程上。最關鍵的是,程序員看不到這些底層的細節,這就降低了編程的難度,提供了更容易的并發。

在Go語言中,協程被稱為Goroutine,它非常輕量,一個Goroutine只占幾KB,并且這幾KB就足夠Goroutine運行完,這就能在有限的內存空間內支持大量Goroutine,從而支持更多的并發。雖然一個Goroutine的棧只占幾KB,但實際是可伸縮的,如果需要更多內存,則runtime會自動為Goroutine分配。

Goroutine的特點,占用內存更小(幾KB)和調度更靈活(runtime調度)。

7 被廢棄的goroutine調度器

Go語言目前使用的調度器是2012年重新設計的,因為之前的調度器性能存在問題,所以使用4年就被廢棄了,那么先來分析一下被廢棄的調度器是如何運作的。

通常用符號G表示Goroutine,用M表示線程。接下來有關調度器的內容均采用圖1.8所示的符號來統一表達。

早期的調度器是基于M:N的基礎上實現的,圖1.9是一個概要圖形,所有的協程,也就是G都會被放在一個全局的Go協程隊列中,在全局隊列的外面由于是多個M的共享資源,所以會加上一個用于同步及互斥作用的鎖。

M想要執行、放回G都必須訪問全局G隊列,并且M有多個,即多線程訪問同一資源需要加鎖進行保證互斥/同步,所以全局G隊列是由互斥鎖進行保護的。

不難分析出來,老調度器有以下幾個缺點:

  • (1) 創建、銷毀、調度G都需要每個M獲取鎖,這就形成了激烈的鎖競爭。
  • (2) M轉移G會造成延遲和額外的系統負載。例如當G中包含創建新協程的時候,M創建了G′,為了繼續執行G,需要把G′交給M2(假如被分配到)執行,也造成了很差的局部性,因為G′和G是相關的,最好放在M上執行,而不是其他M2,如圖1.10所示
  • (3) 系統調用(CPU在M之間的切換)導致頻繁的線程阻塞和取消阻塞操作增加了系統開銷。

在這里插入圖片描述

8 Goroutine調度器的GMP模型的設計思想

面對之前調度器的問題,Go設計了新的調度器。在新調度器中,除了 M(線程)G(協程) ,又引進了 P(處理器)

處理器包含了運行Goroutine的資源,如果線程想運行Goroutine,必須先獲取P,P中還包含了可運行的G隊列。

在這里插入圖片描述

9 GPM模型

在Go中,線程是運行Goroutine的實體,調度器的功能是把可運行的Goroutine分配到工作線程上。
在GPM模型中有以下幾個重要的概念,如圖1.12所示。

在這里插入圖片描述

  • (1)全局隊列(Global Queue): 存放等待運行的G。全局隊列可能被任意的P去獲取里面的G,所以全局隊列相當于整個模型中的全局資源,那么自然對于隊列的讀寫操作是要加入互斥動作的。
  • (2)P的本地隊列: 同全局隊列類似,存放的也是等待運行的G,但存放的數量有限,不超過256個。新建G′時,G′優先加入P的本地隊列,如果隊列滿了,則會把本地隊列中一半的G移動到全局隊列。
  • (3)P列表: 所有的P都在程序啟動時創建,并保存在數組中,最多有GOMAXPROCS(可配置)個。
  • (4)M: 線程想運行任務就得獲取P,從P的本地隊列獲取G,當P隊列為空時,M也會嘗試從全局隊列獲得一批G放到P的本地隊列,或從其他P的本地隊列“偷”一半放到自己P的本地隊列。M運行G,G執行之后,M會從P獲取下一個G,不斷重復下去。

Goroutine調度器和OS調度器是通過M結合起來的,每個M都代表了1個內核線程,OS調度器負責把內核線程分配到CPU的核上執行。

10 有關P和M個數的問題

  • (1) P的數量由啟動時環境變量 $GOMAXPROCS 或者由 runtime 的方法 GOMAXPROCS( ) 決定。這意味著在程序執行的任意時刻都只有 $GOMAXPROCS 個 Goroutine在 同時運行。
  • (2)M的數量由Go語言本身的限制決定,Go程序啟動時會設置M的最大數量,默認為10000個,但是內核很難支持這么多的線程數,所以這個限制可以忽略。runtime/deBug 中的 SetMaxThreads( ) 函數可設置M的最大數量,當一個M阻塞了時會創建新的M。

M與P的數量沒有絕對關系,一個M阻塞,P就會去創建或者切換另一個M,所以,即使P的默認數量是1,也有可能會創建很多個M出來。

11 有關P和M何時被創建

  • (1) P創建的時機在確定了P的最大數量n后,運行時系統會根據這個數量創建n個P。
  • (2) M創建的時機是在當沒有足夠的M來關聯P并運行其中可運行的G的時候。例如所有的M此時都阻塞住了,而P中還有很多就緒任務,就會去尋找空閑的M,如果此時沒有空閑的M,就會去創建新的M。

12 調度器的設計策略

策略一:復用線程

避免頻繁地創建、銷毀線程,而是對線程的復用。

1)偷取(Work Stealing)機制

當本線程無可運行的G時,嘗試從其他線程綁定的P偷取G,而不是銷毀線程,如圖1.13所示

這里需要注意的是,偷取的動作一定是由P發起的,而非M,因為P的數量是固定的,如果一個M得不到一個P,那么這個M是沒有執行的本地隊列的,更談不上向其他的P隊列偷取了。

2)移交(Hand Off)機制

當本線程因為G進行系統調用阻塞時,線程會釋放綁定的P,把P轉移給其他空閑的線程執行,如圖1.14所示,此時若在M1的GPM組合中,G1正在被調度,并且已經發生了阻塞,則這個時候就會觸發移交的設計機制。GPM模型為了更大程度地利用M和P的性能,不會讓一個P永遠被一個阻塞的G1耽誤之后的工作,所以遇見這種情況的時候,移交機制的設計理念是應該立刻將此時的P釋放出來

如圖1.15所示,為了釋放P,所以將P和M1、G1分離,M1由于正在執行當前的G1,全部的程序棧空間均在M1中保存,所以M1此時應該與G1一同進入阻塞的狀態,但是已經被釋放的P需要跟另一個M進行綁定,所以就會選擇一個M3(如果此時沒有M3,則會創建一個新的或者喚醒一個正在睡眠的M)進行綁定,這樣新的P就會繼續工作,接收新的G或者從其他的隊列中實施偷取機制。

策略二:利用并行

GOMAXPROCS 設置P的數量,最多有 GOMAXPROCS 個線程分布在多個CPU上同時運行。 GOMAXPROCS 也限制了并發的程度,例如 GOMAXPROCS=核數/2 ,表示最多利用一半的CPU核進行并行。

策略三:搶占

在Co-routine中要等待一個協程主動讓出CPU才執行下一個協程,在Go中,一個Goroutine最多占用CPU 10ms,防止其他Goroutine無資源可用,這就是Goroutine不同于Co-routine的一個地方。

Co-routine(C語言中的協程),用戶態線程。
coroutine 是基于 ucontext 的一個 C 語言協程庫實現

策略四:全局G隊列

在新的調度器中依然有全局G隊列,但功能已經被弱化了,當M執行偷取,但從其他P偷不到G時,它可以從全局G隊列獲取G。

13 go func() 調度流程

如果執行一行代碼 go func( ) ,則在GPM模型上的概念里會執行哪些操作。

(1)通過 go func( ) 創建一個Goroutine,

(2)有兩個存儲G的隊列,一個是局部調度器P的本地隊列,另一個是全局G隊列。新創建的G會先保存在P的本地隊列中,如果P的本地隊列已經滿了,就會保存在全局的隊列中,如圖1.19所示。

(3)G只能運行在M中,一個M必須持有一個P,M與P是1:1的關系。M會從P的本地隊列彈出一個可執行狀態的G來執行,如果P的本地隊列為空,則會從全局隊列進行獲取,如果從全局隊列獲取不到,則會向其他的MP組合偷取一個可執行的G來執行,如圖1.20所示。

(4)一個M調度G執行的過程是一個循環機制,如圖1.21所示。

(5)當M執行某一個G時如果發生了syscall或者其余阻塞操作,則M會阻塞,如果當前有一些G在執行,runtime則會把這個線程M從P中移除(Detach),然后創建一個新的操作系統線程(如果有空閑的線程可用就復用空閑線程)來服務于這個P。

(6)當M系統調用結束時,這個G會嘗試獲取一個空閑的P執行,并放入這個P的本地隊列。如果獲取不到P,則這個線程M會變成休眠狀態,加入空閑線程中,然后這個G會被放入全局隊列中。

14 調度器的生命周期

在Go語言調度器的GPM模型中還有兩個比較特殊的角色,它們分別是M0和G0。

M0

(1)啟動程序后的編號為0的主線程。

(2)在全局命令runtime.m0中,不需要在heap堆上分配。

(3)負責執行初始化操作和啟動第1個G。

(4)啟動第1個G后,M0就和其他的M一樣了。

G0

(1)每次啟動一個M,創建的第1個Goroutine就是G0。

(2)G0僅用于負責調度G。

(3)G0不指向任何可執行的函數。

(4)每個M都會有一個自己的G0。

(5)在調度或系統調度時,會使用M切換到G0,再通過G0調度。

(6)M0的G0會放在全局空間。

一個Goroutine的創建周期如果加上M0和G0的角色,則整體的流程如圖1.24所示。

下面跟蹤一段代碼,對調度器里面的結構做一個分析,代碼如下:

package mainimport "fmt"func main() {fmt.Println("Hello world")
}

整體的分析過程如下:

(1)runtime創建最初的線程 M0Goroutine G0 ,并把二者關聯。

(2)調度器初始化:初始化M0、棧、垃圾回收,以及創建和初始化由 GOMAXPROCSP 構成的 P列表 ,如圖1.25所示。

(3)示例代碼中的 main( ) 函數是 main.main ,runtime中也有1個main()函數 runtime.main ,代碼經過編譯后, runtime.main 會調用 main.main ,程序啟動時會為 runtime.main 創建Goroutine,稱為 Main Goroutine ,然后把 Main Goroutine 加入P的本地隊列。

(4)啟動M0,M0已經綁定了P,會從P的本地隊列獲取G,并獲取 Main Goroutine

(5)G擁有棧,M根據G中的棧信息和調度信息設置運行環境。

(6)M運行G。

(7)G退出,再次回到M獲取可運行的G,這樣重復下去,直到 main.main 退出, runtime.main 執行Defer和Panic處理,或調用 runtime.exit 退出程序。

調度器的生命周期幾乎占滿了一個Go程序的一生, runtime.main 的Goroutine執行之前都是為調度器做準備工作, runtime.main 的Goroutine運行才是調度器的真正開始,直到 runtime.main 結束而結束。

參考

  • https://blog.csdn.net/flynetcn/article/details/126628952
  • https://blog.csdn.net/weixin_43495948/article/details/129415438
  • 《深入理解Go語言》劉丹冰

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/715477.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/715477.shtml
英文地址,請注明出處:http://en.pswp.cn/news/715477.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

chatgpt:還有哪些人工智能和科技值得關注?

今天,很多人的目光都被ChatGPT吸引,其實,人工智能的范圍很大,遠不止ChatGPT或者其他自然語言的處理工具。所以說不管ChatGPT的結果如何,人工智能依然是未來。 那么在ChatGPT之外,還有沒有什么值得關注的人…

在網頁上踢球:打造我自己的python(Django)足球網站

足球不僅僅是球場上的90分鐘。它是一個不斷發展的故事,一個全球球迷社群的粘合劑,一個數據和熱情交織的世界。作為一名開發者和球迷,我決定將這兩大愛好結合起來,用 Django 打造一個足球網站,讓球迷們能夠追蹤他們最愛…

Unity AI生成全景圖制作天空盒

現在的AI很強大。 其中,有這樣一個網站,通過輸入提示詞,選擇某種風格就可以為你生成360全景圖。 網頁鏈接 一、生成全景圖 打開網頁后,如圖: 勾選,點擊CONFIRM。 點擊GET STARTED,進入主頁。…

機器人定位——里程計Odom

根據兩個車輪的輪速去估計當前的車的定位 我將提供一個更詳細完整的模型來描述兩輪差速機器人的里程計數。 我們假設機器人的兩個輪子的半徑分別為r1和r2,兩個輪子的轉速分別為ω1和ω2。機器人的輪距為L,指的是兩個輪子中心之間的距離。 首先&#x…

Git LFS配置

當你需要克隆一個包含通過 Git Large File Storage (LFS) 管理的大文件的倉庫時,確保 Git LFS 已經在你的系統上安裝并正確配置是很重要的。這樣,當你執行 git clone 命令時,Git LFS 跟蹤的文件也會被正確地下載。以下是在 macOS 上進行配置和…

Stable Cascade-ComfyUI中文生圖、圖生圖、多圖融合基礎工作流分享

最近 ComfyUI對于Stable Cascade的支持越來越好了一些,官方也放出來一些工作流供參考。 這里簡單分享幾個比較常用的基礎工作流。 (如果還沒有下載模型,可以先閱讀上一篇Stable Cascade升級,現在只需要兩個模型) &a…

python數據分析numpy基礎之argmax求數組最大值索引

1 python數據分析numpy基礎之argmax求數組最大值索引 python的numpy庫的argmax()函數&#xff0c;用于獲取沿指定軸的最大值的索引。 用法 numpy.argmax(a, axisNone, outNone, *, keepdims<no value>)描述 argmax()返回沿指定軸的最大值的索引。 入參axis表示指定軸…

Docker技術概論(5):Docker網絡

Docker技術概論&#xff08;5&#xff09; Docker網絡 - 文章信息 - Author: 李俊才 (jcLee95) Visit me at CSDN: https://jclee95.blog.csdn.netMy WebSite&#xff1a;http://thispage.tech/Email: 291148484163.com. Shenzhen ChinaAddress of this article:https://blog…

基于QT和Visa的安捷倫(keysight)34970A溫度采集

在以前的文章中&#xff0c;描述了如何在labview開發讀取34970A儀器采集的溫度。 也描述了如何安裝keysight IO Libraries Suits. 那么本文更進一步&#xff0c;描述QT平臺c語言開發軟件&#xff0c;讀取34970A儀器采集的溫度。 以下是c代碼&#xff0c;因為采集耗費時間長&…

C++虛函數調用規則

C虛函數調用規則 基類、派生類結構&#xff1a; class Foo { public:virtual void print() {cout << "Foo" << endl;} }; class Bar : public Foo { public:virtual void print() {cout << "Bar" << endl;} };1.通過對象直接調用…

AcWing 895. 最長上升子序列(線性dp)

問題描述 給定一個長度為N NN的數列&#xff0c;求數值嚴格單調遞增的子序列的長度最長是多少。 輸入格式&#xff1a; 第一行包含整數N NN。 第二行包含N NN個整數&#xff0c;表示完整序列。 輸出格式&#xff1a; 輸出一個整數&#xff0c;表示最大長度。 數據范圍 1 ≤…

【C++提高編程】

C提高編程 C提高編程1 模板1.1 模板的概念1.2 函數模板1.2.1 函數模板語法1.2.2 函數模板注意事項1.2.3 函數模板案例1.2.4 普通函數與函數模板的區別1.2.5 普通函數與函數模板的調用規則1.2.6 模板的局限性 1.3 類模板1.3.1 類模板語法1.3.2 類模板與函數模板區別1.3.3 類模板…

備戰藍橋杯---動態規劃的一些思想1

話不多說&#xff0c;直接看題&#xff1a; 目錄 1.雙線程DP 2.正難則反多組DP 3.換個方向思考&#xff1a; 1.雙線程DP 可能有人會說直接貪心&#xff1a;先選第1條的最優路徑&#xff0c;再選第2條最優路徑。 其實我們再選第1條時&#xff0c;我們怎么選會對第2條的路徑…

FastJson中“$ref 循環引用檢測”的問題

今天在測試時&#xff0c;錯誤停留在了以下的代碼行 Object object new ObjectMapper().readValue(JSON.toJSONString(procInst.getForm()), Object.class); 報錯信息&#xff1a;com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field &quo…

linux命令行與shell腳本大全——學習筆記(1-4章)

第一章、第二章 查看運行層級 runlevel 目前有7個層級&#xff0c;3是有聯網的多用戶模式&#xff0c;5是配有GUI的多用戶模式&#xff0c;等等 第三章 啟動shell 查看/etc/passwd文件&#xff0c;可以看到每個用戶的默認shell程序&#xff0c;如: christine:x:1001:1001:…

面條機水箱低液位提醒功能如何實現

光電液位傳感器在面條機水箱低液位功能的實現中發揮著重要作用。該技術通過光學原理和分離式設計&#xff0c;實現了面條機水箱液位的精準檢測和智能控制&#xff0c;為面條生產提供了穩定的保障。 采用分離式液位傳感器&#xff0c;將菱鏡部分設計直接置于面條機水箱上&#…

nvidia a100-pcie-40gb環境安裝

1.conda create --name torch_li python3.8 2. conda install pytorch1.7.1 torchvision0.8.2 torchaudio0.7.2 cudatoolkit11.0 -c pytorch 環境測試&#xff1a;torch.cuda.is_available() 3.conda remove -n torch_li --all 4.pip install opencv-python-headless 5.pip ins…

SOCKS55代理與Http代理有何區別?如何選擇?

在使用IPFoxy全球代理時&#xff0c;選擇 SOCKS55代理還是HTTP代理&#xff1f;IPFoxy代理可以SOCKS55、Http協議自主切換&#xff0c;但要怎么選擇&#xff1f;為解決這個問題&#xff0c;得充分了解兩種代理的工作原理和配置情況。 在這篇文章中&#xff0c;我們會簡要介紹 …

overleaf上傳到arxiv 參考文獻無法引用(?)

記一下overleaf上傳到arxiv的bug 參考文獻無法引用&#xff08;&#xff1f;&#xff09; 因為需要上傳bbl文件而不是bib 用overleaf生成bbl 另外需要將bbl和txt的文件名設置成一樣的

Linux筆記--解壓縮

一、tar指令 Linux打包文件通常以.tar結尾&#xff0c;壓縮文件以.gz(.bz2)結尾。通常壓縮和打包是一起進行的&#xff0c;打包壓縮后文件后綴名一般為.tar.gz。 z∶使用gzip進行解壓縮 j:使用bzip2進行解壓縮 c: create&#xff0c;創建文件 x : extract&#xff0c;解壓 v:…