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來運行。
注:以上部分信息和圖片來自The Go scheduler。