Go 面試官:GMP 模型,爲何要有 P?

微信搜索【 腦子進煎魚了】關注這一隻爆肝煎魚。本文 GitHub github.com/eddycjy/blog 已收錄,有個人系列文章、資料和開源 Go 圖書。

你們好,我是煎魚。git

最近金三銀四,是面試的季節。在個人 Go 讀者交流羣裏出現了許多小夥伴在討論本身面試過程當中所遇到的一些 Go 面試題。github

今天的主角,是 Go 面試的萬能題 GMP 模型的延伸題(疑問),那就是 」GMP 模型,爲何要有 P?「golang

進一步推敲問題的背後,其實這個面試題本質是想問:」GMP 模型,爲何不是 G 和 M 直接綁定就完了,還要搞多一個 P 出來,那麼麻煩,爲的是什麼,是要解決什麼問題嗎?「面試

這篇文章煎魚就帶你一同探索,GM、GMP 模型的變遷是由於什麼緣由。算法

GM 模型

在 Go1.1 以前 Go 的調度模型其實就是 GM 模型,也就是沒有 P。緩存

今天帶你們一塊兒回顧過去的設計。服務器

解密 Go1.0 源碼

咱們瞭解一個東西的辦法之一就是看源碼,和煎魚一塊兒看看 Go1.0.1 的調度器源碼的核心關鍵步驟:微信

static void
schedule(G *gp)
{
    ...
    schedlock();
    if(gp != nil) {
        ...
        switch(gp->status){
        case Grunnable:
        case Gdead:
            // Shouldn't have been running!
            runtime·throw("bad gp->status in sched");
        case Grunning:
            gp->status = Grunnable;
            gput(gp);
            break;
        }

    gp = nextgandunlock();
    gp->readyonstop = 0;
    gp->status = Grunning;
    m->curg = gp;
    gp->m = m;
    ...
    runtime·gogo(&gp->sched, 0);
}
  • 調用 schedlock 方法來獲取全局鎖。
  • 獲取全局鎖成功後,將當前 Goroutine 狀態從 Running(正在被調度) 狀態修改成 Runnable(能夠被調度)狀態。
  • 調用 gput 方法來保存當前 Goroutine 的運行狀態等信息,以便於後續的使用;
  • 調用 nextgandunlock 方法來尋找下一個可運行 Goroutine,而且釋放全局鎖給其餘調度使用。
  • 獲取到下一個待運行的 Goroutine 後,將其的運行狀態修改成 Running。
  • 調用 runtime·gogo 方法,將剛剛所獲取到的下一個待執行的 Goroutine 運行起來。

思考 GM 模型

經過對 Go1.0.1 的調度器源碼剖析,咱們能夠發現一個比較有趣的點。那就是調度器自己(schedule 方法),在正常流程下,是不會返回的,也就是不會結束主流程。網絡

G-M模型簡圖

他會不斷地運行調度流程,GoroutineA 完成了,就開始尋找 GoroutineB,尋找到 B 了,就把已經完成的 A 的調度權交給 B,讓 GoroutineB 開始被調度,也就是運行。併發

固然了,也有被正在阻塞(Blocked)的 G。假設 G 正在作一些系統、網絡調用,那麼就會致使 G 停滯。這時候 M(系統線程)就會被會從新放內核隊列中,等待新的一輪喚醒。

GM 模型的缺點

這麼表面的看起來,GM 模型彷佛牢不可破,毫完好陷。但爲何要改呢?

在 2012 年時 Dmitry Vyukov 發表了文章《Scalable Go Scheduler Design Doc》,目前也依然成爲各大研究 Go 調度器文章的主要對象,其在文章內講述了總體的緣由和考慮,下述內容將引用該文章。

當前(代指 Go1.0 的 GM 模型)的 Goroutine 調度器限制了用 Go 編寫的併發程序的可擴展性,尤爲是高吞吐量服務器和並行計算程序。

實現有以下的問題:

  • 存在單一的全局 mutex(Sched.Lock)和集中狀態管理:

    • mutex 須要保護全部與 goroutine 相關的操做(建立、完成、重排等),致使鎖競爭嚴重。
  • Goroutine 傳遞的問題:

    • goroutine(G)交接(G.nextg):工做者線程(M's)之間會常常交接可運行的 goroutine。
    • 上述可能會致使延遲增長和額外的開銷。每一個 M 必須可以執行任何可運行的 G,特別是剛剛建立 G 的 M。
  • 每一個 M 都須要作內存緩存(M.mcache):

    • 會致使資源消耗過大(每一個 mcache 能夠吸納到 2M 的內存緩存和其餘緩存),數據局部性差。
  • 頻繁的線程阻塞/解阻塞:

    • 在存在 syscalls 的狀況下,線程常常被阻塞和解阻塞。這增長了不少額外的性能開銷。

GMP 模型

爲了解決 GM 模型的以上諸多問題,在 Go1.1 時,Dmitry Vyukov 在 GM 模型的基礎上,新增了一個 P(Processor)組件。而且實現了 Work Stealing 算法來解決一些新產生的問題。

GMP 模型,在上一篇文章《Go 羣友提問:Goroutine 數量控制在多少合適,會影響 GC 和調度?》中已經講解過了。

以爲不錯的小夥伴能夠關注一下,這裏就再也不復述了。

帶來什麼改變

加了 P 以後會帶來什麼改變呢?咱們再更顯式的講一下。

  • 每一個 P 有本身的本地隊列,大幅度的減輕了對全局隊列的直接依賴,所帶來的效果就是鎖競爭的減小。而 GM 模型的性能開銷大頭就是鎖競爭。
  • 每一個 P 相對的平衡上,在 GMP 模型中也實現了 Work Stealing 算法,若是 P 的本地隊列爲空,則會從全局隊列或其餘 P 的本地隊列中竊取可運行的 G 來運行,減小空轉,提升了資源利用率。

爲何要有 P

這時候就有小夥伴會疑惑了,若是是想實現本地隊列、Work Stealing 算法,那爲何不直接在 M 上加呢,M 也照樣能夠實現相似的組件。爲何又再加多一個 P 組件?

結合 M(系統線程) 的定位來看,若這麼作,有如下問題:

  • 通常來說,M 的數量都會多於 P。像在 Go 中,M 的數量默認是 10000,P 的默認數量的 CPU 核數。另外因爲 M 的屬性,也就是若是存在系統阻塞調用,阻塞了 M,又不夠用的狀況下,M 會不斷增長。
  • M 不斷增長的話,若是本地隊列掛載在 M 上,那就意味着本地隊列也會隨之增長。這顯然是不合理的,由於本地隊列的管理會變得複雜,且 Work Stealing 性能會大幅度降低。
  • M 被系統調用阻塞後,咱們是指望把他既有未執行的任務分配給其餘繼續運行的,而不是一阻塞就致使所有中止。

所以使用 M 是不合理的,那麼引入新的組件 P,把本地隊列關聯到 P 上,就能很好的解決這個問題。

總結

今天這篇文章結合了整個 Go 語言調度器的一些歷史狀況、緣由分析以及解決方案說明。

」GMP 模型,爲何要有 P「 這個問題就像是一道系統設計瞭解,由於如今不少人爲了應對面試,會硬背 GMP 模型,或者是泡麪式過了一遍。而理解其中真正背後的緣由,纔是咱們要去學的要去理解。

知其然知其因此然,纔可破局。

如有任何疑問歡迎評論區反饋和交流,最好的關係是互相成就,各位的點贊就是煎魚創做的最大動力,感謝支持。

文章持續更新,能夠微信搜【腦子進煎魚了】閱讀,回覆【 000】有我準備的一線大廠面試算法題解和資料;本文 GitHub github.com/eddycjy/blog 已收錄,歡迎 Star 催更。
相關文章
相關標籤/搜索