1. 程式人生 > 其它 >Golang-排程器原理解析

Golang-排程器原理解析

Golang排程器原理解析

本文主要介紹排程器的由來以及golang排程器為何要如此設計,以及GPM模型解析

一.排程器的由來

1.單程序時代

單程序時代不需要排程器,一切程式都是序列,所以單程序的作業系統會面臨這樣一個問題:

  • 程式只能序列執行,一個程序阻塞了,其他程序啥事也做不了,只能等待,會造成CPU時間的嚴重浪費

那麼能不能有多個程序一起來執行多個任務呢?
答案是可以的,後來作業系統就有了最早的併發能力:多程序併發
多程序併發:當一個程序阻塞的時候,切換到另外等待執行的程序,儘量將CPU利用起來。

2.多程序/多執行緒時代

多程序或多執行緒時代就有了排程器的需求,以多程序為例,其會使用CPU排程器來當某個程序阻塞的時候,排程一個合適的程序給CPU。

這種方式解決了阻塞的問題,但也存在一個問題:

  • 如果程序數量很多,程序的排程會佔用CPU很多的時間(程序建立,銷燬,切換等),CPU利用率不高

對比執行緒,雖然其排程成本會比程序小很多,但實際上多執行緒程式的開發和設計也比較複雜,而且在當前網際網路業務環境下,為每個任務都建立一個執行緒是不現實的,這會大量的消耗記憶體(程序佔用4G(32位),而執行緒大約也要4M)
所以,多執行緒/多程序時代,會面臨這樣兩個問題

  • 高記憶體佔用
  • 排程的高CPU消耗

但是,其實一個執行緒可以分為核心態執行緒使用者態執行緒一個使用者態執行緒必須要繫結一個核心態執行緒,但是CPU並不知道使用者態執行緒的存在,它只知道它執行的是一個核心態執行緒(Linux的PCB程序控制塊)

3.協程時代

我們可以將核心執行緒依然叫做執行緒,把使用者態執行緒叫做協程

那麼協程和執行緒就有三種對映關係:

  • N : 1 : N個協程,一個執行緒
  • 1 : 1 : 一個協程,一個執行緒
  • N : M : N個協程,M個執行緒
    下面我們分別討論一下這三種對映關係的優點和缺點:

N : 1 關係

N個協程繫結一個執行緒

優點:

  • 協程在使用者態即完成切換,不會陷入到核心態,這種切換非常輕量快速

缺點:

  • 無法使用硬體的多核加速
  • 一旦協程阻塞,造成執行緒阻塞,本程序的其他協程就都無法執行了,根本沒有併發能力!

1 : 1 關係

一個協程繫結一個執行緒

優點:

  • 容易實現,不存在N比1的缺點

缺點:

  • 協程的建立,刪除,切換的代價都由CPU完成,代價有點昂貴

M : N 關係

M個協程,N個執行緒

克服了以上兩種模型的缺點,但是實現較為複雜

協程和執行緒的排程是有區別的,執行緒是由CPU排程的,是搶佔式的,協程是由使用者態排程,是協作式的,一個協程讓出CPU後,才執行下一個協程

二. goroutine 和 go排程器

1. goroutine

go提供了goroutine

goroutine來自於協程的概念,讓一組可複用的函式執行在一組執行緒之上,即使有協程被阻塞,該執行緒的其他協程也可以被runtime排程,轉移到其他可執行的執行緒上,最關鍵的是,程式設計師看不到這些底層的細節,這就降低了程式設計的難度,提供了更容易的併發

goroutine非常的輕量,一個goroutine只佔幾KB,並且只幾KB就足夠goroutine執行完,這樣就能在有限的記憶體空間內,支援大量的goroutine,支援更多的併發,雖然一個goroutine的棧只有幾KB,但實際是可伸縮的,如果goroutine需要更多的空間,runtime會為goroutine自動分配。

goroutine的特點:

  • 佔用記憶體很小(幾KB)
  • 排程更加靈活(由runtime排程)

2. go排程器的演變歷史

go目前使用的排程器是2012年重新設計的,因為之前的排程器存在效能問題,我們先來研究一下廢棄的排程器,這樣才能更好的瞭解現有的排程器為何如此設計

先行約定一下,我們採用G來表示goroutine,M來表示執行緒

廢棄的排程器僅有一個全域性的go協程佇列,所以多個M如果要訪問此全域性的G佇列,都需要加鎖,鎖的粒度會非常的大,極度的影響排程器的效能,所以我們可以總結一下,老排程器的幾個缺點:

  • 鎖競爭激烈: 每個M要需要加鎖訪問全域性的G佇列
  • 延遲和額外的系統負載:比如G中建立新的協程的時候,最好是新建的協程能給當前M,而不是其他M,區域性性很差
  • 系統呼叫(CPU在M之間切換),導致頻繁的系統阻塞和取消阻塞的操作,都增加了系統的開銷
    正是基於以上缺點的改進,GPM模型的go排程器,誕生了!

三.GPM模型的Go排程器及其設計思想

在GPM模型的go排程器中,除了M和G,又引進了P

  • G : 協程
  • P : 邏輯處理器,包含了執行goroutine的資源和可執行的G佇列
  • M : 核心執行緒,負責執行G
  • 全域性佇列:存放等待執行的G
  • P的本地佇列:和全域性佇列類似,存放的也是等待執行的G,但是存放的數量有限,不會超過256個,在G中新建G時,新建的G優先加入P的本地佇列,如果佇列滿了,則把本地佇列中一半的G移動到全域性佇列
  • P列表:所有的P都在程式啟動時建立,並保證的陣列中,最多有GOMAXPROCS
  • M:執行緒想執行G就得獲得P,從P的本地佇列獲取G,P佇列為空時,M會嘗試從全域性佇列拿一批G放到P的本地佇列,或從其他P的本地佇列偷取一般放入自己P的本地佇列

goroutine排程器和OS排程器是通過M結合起來的,每個M都代表了一個核心執行緒,OS排程器負責把核心執行緒分配到CPU的核上執行

一.go排程器的設計思想:

1.複用執行緒

避免頻繁的建立,銷燬執行緒,而是對執行緒進行復用

  • work stealing 機制 :當M繫結的P佇列中可執行的G時,嘗試從其他M繫結的P佇列中偷取G,而不是銷燬M
  • hand off機制 : 當M進行系統呼叫而阻塞時,執行緒釋放繫結的P

2.利用並行

GOMAXPROCS設定P的數量,最多有GOMAXPROCS個執行緒分佈在多個CPU上同時執行,GOMAXPROCS同時也限制了併發的程度,比如GOMAXPROCS=核數/2,則最多利用了一半的CPU核進行並行

3.協作排程

在coroutine中要等待一個協程主動讓出CPU才執行下一個協程,但是在go中,一個goroutine最多佔用CPU 10ms,防止其他的goroutine餓死,這就是goroutine不同於coroutine的一個地方

4.全域性G佇列

在新的排程器中仍然有全域性G佇列,但功能已經被弱化了,當M執行work stealing從其他P偷不到G時,它可以從全域性G佇列獲取G

二.啟動一個goroutine的排程流程

通過上圖,我們可以得到幾個結論:

  • 通過go關鍵字來啟動一個goroutine
  • 有兩類G的儲存佇列,一個是P的區域性G佇列,一個是全域性G佇列,新建G會儲存在P的本地G佇列中,如果P的本地G佇列滿了就會儲存在全域性的G佇列中
  • G只能執行在M中,一個M必須持有一個P,M與P的關係是一比一,M會從P的本地G佇列中彈出一個G來執行,如果P的本地佇列為空,就會想衝其他的MP組合中偷取G來執行
  • 一個M排程G執行的過程是一個迴圈機制
  • 當M執行每一個G的時候如果發生了系統呼叫或阻塞操作,那麼這個M會被阻塞,如果當前有一些G在這個MP組合,runtime會吧這個M從P中摘除,然後再建立一個新的M或者尋找一個空閒的M來服務P
  • 當M的系統呼叫或阻塞操作結束的時候,這個G會嘗試獲取一個空閒的P,並放入到這個P的本地佇列,如果獲取不到P,則此M變成休眠狀態,加入到空閒M中,然後這個G會被放到全域性的G佇列中

三.特殊的M0和G0

  • M0:M0是啟動程式後編號為0的執行緒,M0負責執行初始化操作和啟動第一個G,M0對應的例項會在全域性遍歷runtime.m0中
  • G0:每個M都有自己的G0,G0僅負責排程G,不執行其他任何可執行的函式,每啟動一個M,都會建立屬於此M的G0

四.有關P和M數量的問題

  • P的數量
    • 由啟動時環境變數\(GOMAXPROCS***或者***runtime***的***GOMAXPROCS***()決定,這意味著在程式執行的任意時刻都只有***\)GOMAXPROCSgoroutine在同時執行
  • M的數量
    • go語言本身的限制:go程式啟動時,會設定M的最大數量,預設10000,但是核心很難支援這麼多執行緒數,所以這個限制可以忽略
    • runtime/debug中serMaxThreads函式,設定M的最大數量
    • 一個M阻塞了,會建立新的M

M和P的數量沒有絕對關係,一個M阻塞,P就會去建立或者切換到另外一個M,所以,即使P的預設數量是1,也有可能會建立很多個M出來

五.P和M何時會被建立

  • P何時建立
    • 在確定P的最大數量N之後,runtime會根據這個建立N個P
  • M何時建立
    • 沒有足夠的M來關聯P,並執行其中可執行的G時,比如所有的M都阻塞住了,而P中還有很多待執行的G,就會去尋找空閒的M,沒有空閒的M就會去建立新的M

四.總結

Go排程本質是把大量的goroutine分配到少量執行緒上去執行,並且利用多核並行,實現強大的併發

心之所向,素履以往