談談Golang中goroutine的調度問題

goroutine的調度問題,一樣也是我以前面試的問題,不過這個問題我當時並非很清楚,回來之後立馬查閱資料,現整理出來備忘。面試

有一些預備知識須要說明,就是操做系統中的線程。操做系統中的線程分爲兩種:內核線程和用戶線程。用戶平時使用的線程並非內核線程,而是存在於用戶態的用戶線程。用戶線程並不必定在操做系統內核中對用同等數量的內核線程。這裏有三個模型:緩存

1.一對一模型(1:1)多線程

2. 多對一模型(N:1)併發

3. 多對多模型(N:M)性能

下面就先來談談這三種線程模型。spa

1.一對一模型(1:1)操作系統

對於支持線程的操做系統來講,一對一模型是最簡單的一種線程模型了,一個用戶線程惟一對應一個內核線程,但反過來卻不必定,一個內核線程並不必定有對應的用戶線程存在。這樣一來,因爲一個內核線程至多隻對應一個用戶線程,線程之間能夠作到最大程度的併發,不一樣線程之間不會相互影響,好比一個線程阻塞了也不會影響到其餘線程的執行。對於多處理器,一對一的線程模型效率更高。可是不少操做系統限制了內核線程的數量,若是採用一對一模型,用戶線程的數量也會受到比較大的限制。並且不少操做系統的內核線程在調度時開銷較大,這也會影響用戶線程的效率。線程

2. 多對一模型(N:1)3d

多對一模型意味着多個用戶線程對應一個內核線程,用戶線程間的切換由代碼控制,由於線程間切換的效率比較高(不用陷入內核區去切換)。不過若是其中一個用戶線程阻塞了,則和它對應相同內核線程的那些用戶線程也都會阻塞,由於內核線程是被共用的(且是綁定的),此時它沒法抽身出來。並且增長處理器個數對於多對一線程模型幫助也不大,畢竟在這種狀況下,一個線程阻塞,相關線程也跟着遭殃的事實和處理器個數關係不大。這種模型的好處是線程間切換開銷低,且線程數量能夠不少。指針

3. 多對多模型(N:M)

多對多線程模型能夠說是上面兩種模型的結合,也是最複雜的,它把多個用戶線程對應到多個內核線程上,且不少時候不是惟一綁定的。所以一個內核線程在一個時間點能夠對應0到多個用戶線程。且在運行期間,系統能夠根據線程執行狀況作合理分配。好比用戶線程一、用戶線程2和用戶線程3對應到一個內核線程1,若是用戶線程1阻塞了,系統能夠調度用戶線程2和用戶線程3到其餘內核線程上去,這是個動態的過程。多對多線程模型的優點是可讓系統資源獲得比較均衡的使用,用戶線程之間互相影響比較小,且在多處理器上表現不錯(雖然增長處理器個數對它性能提高可能不如一對一模型那麼高),關鍵是它很靈活。

Golang的goroutine調度和多對多模型密切相關,Golang本身有本身的調度器scheduler。Golang的調度器內部有三個重要結構:M、P和G。

M: 表明內核線程。

G: 表明一個goroutine,它有本身的棧,指令指針和一些基本信息,用於被調度。

P: 表明調度的上下文,是Golang內部的調度器,負責讓多個goroutine在一個內核線程上運行,它實現了N:1到N:M。

能夠看到在某個時刻,一個M對應到一個P,一個P上有一個正在運行的G(藍色的G),且這個P上可能還有多個G等待被調度(灰色的G),P維護着這個調度隊列(runqueue)。P的數量能夠經過GOMAXPROCS()來設置,它其實也就表明了真正的併發度,即有多少個goroutine能夠同時運行。不過須要注意,GOMAXPROCS()的最大值是256。當要啓動一個goroutine時,只須要用go function(args)便可,一但咱們啓動了一個goroutine,就會在runqueue隊尾加入這個goroutine,P會負責調度這些goroutine。

那麼若是在某個M被阻塞了呢?這時候就是N:M模型的關鍵之處了,此時P能夠被安排到其餘M上去執行,因爲P內部維護着一些G的信息,這些G都有獨立的棧和指令指針這些基本信息,因此能夠很方便地直接換到另外一個未被阻塞的M下。

上圖描述了這種狀況,在左邊,G0正在運行,當G0因爲系統調用被阻塞時,調度器會建立或者從線程緩存中取得一個線程M1,轉投M1。當G0返回時,它必須得到一個P來執行,此時通常是先查看系統中有沒有空閒的P,若是有,就得到一個P,用這個P來執行,若是沒能得到一個P,這個G0只能暫時放置到一個全局的執行隊列(global runqueue)中,它所處的線程M0也就sleep了。系統中的P們會週期性地檢查這個隊列,取出裏面的G來運行。

如上圖,還有一種狀況是,某個M上的P被分配的G很快就被執行完了,這時M和P都處於閒置狀態,無事可作。但也不可能看着其餘的P忙碌本身不幫忙吧,此時會先去global runqueue中檢查是否有G能夠拿,若是並無,只好嘗試去其餘P上「偷」一些G來執行,通常就是偷對方runqueue大小的一半,若是仍是沒偷到,那該只好sleep,該P放入一個閒置的P隊列結構等待再次被調度。
 
這樣處理,就保證了系統中的G能被有效快速地執行,充分利用了系統資源。

注:以上部分信息和圖片來自The Go scheduler

相關文章
相關標籤/搜索