談談對 GMP 的簡單認識

語言: CN / TW / HK

theme: scrolls-light

猶記得最開始學習 golang 的時候,大佬們分享 GMP 模型的時候,總感覺雲裡霧裡,聽了半天,並沒有一個很清晰的概念,不知 xmd 是否會有這樣的體會

雖然 golang 入門很簡單,但是對於理解 golang 的設計思想和原理,還是需要一定時間的積累和沉澱,更多的應該是思想上的沉澱

希望這篇文章能夠對你瞭解 golang 的 GMP 模型有一點幫助

文章分別從一下三個方面來談談我對 GMP 模型認識

  • golang 中排程器的變化及其作用
  • 有了程序,執行緒,為什麼會出現協程
  • GMP 模型中的 G,M,P 分別都做著什麼樣的事情

golang 中的排程器的變化及其作用

排程器,scheduler

怎麼理解呢?排程器就像是一個管理者,負責安排事項,負責排程不同人在指定時間在某個崗位上完成自己的價值交付

正如 linux 排程器一樣,將就緒的程序排程成執行狀態,或者將執行狀態的程序,打斷,變成阻塞狀態,再變成就緒狀態

比如說一個經典的單程序 和 多程序/多執行緒的作業系統,

我們可以看到在單程序系統中,只需要無腦的將程序序列排列好, CPU 會序列去執行任務,如果遇到程序1 阻塞的情況,其他程序也沒有辦法被 cpu 執行,那麼程序2 ,程序3 ,程序4 就都要等待前面的程序完成執行完畢,才能到自己執行

可以看出單程序對於 CPU 的使用過於任性,浪費 CPU 的資源,演進到多程序/多執行緒作業系統的時候,就出現了排程器

上圖中我們可以對比看到,在多程序/多執行緒的作業系統中,cpu 的時間片被分割的更加的小,對於 cpu 資源的利用率是大大的增加了,因為 cpu 可以在程序1阻塞的時候,切換去執行程序2

例如,當程序1 執行過程中,發生了阻塞,那麼排程器就會就會將 cpu 切換到程序2 中進行執行,同理,程序2 阻塞的時候, cpu 就會被切換到程序 3 進行執行,當然,這就看是哪個程序先搶到 cpu 資源了

可以看到,排程器在這裡的作用就是最大限度的利用上 CPU 的資源,管理程序在 CPU 上按照一定的的順序執行任務,就好比一個優秀的管家可以合理安排好不同的員工在指定的時間上專注的處理某項事務

那麼 golang 的排程器是不是也是和 linux 中的排程器有著想通之處呢?

來看看排程器在 golang 中的具體作用是幹啥的

在 golang 中,排程器的實現簡單來看實際上是由協程和執行緒按照一定的邏輯來組合起來的,其實也是扮演著一個協調和排程的作用,排程的物件是協程和執行緒,協程是需要被排程到執行緒中來執行的,這個動作就是排程器乾的

  • 通常用 G 來表示協程
  • 用 M 來表示執行緒

正如是這樣來實現排程器的:

我們可以看到,當每一個 M 想要從全域性佇列中取 G 出來執行的時候,是需要訪問全域性佇列的鎖,這是一把互斥鎖,同一時間只能有 1 個 M 在訪問

Golang 的排程器此處當然也能讓多個 M 處理不同的 G,達到多程序/多執行緒 併發處理事項的目的

可我們知道,加鎖,是會影響到我們系統的整體效能的,畢竟那麼多 M 都在競爭這一把鎖,勢必同一時間沒有搶到的鎖的 M 就要等待了

為什麼排程器會被淘汰掉?

看到上圖,細心的我們可以發現,

所以,這個排程器缺點也是比較明顯的:

第一,多個 M 都要併發的去獲取全域性佇列中的 G ,會造成鎖競爭 , 此處操作全域性佇列的情況有如下幾種情況

  • 建立 G
  • 排程 G
  • 銷燬 G

第二, 執行緒 M 在執行 G 的過程中,如果 G 阻塞了,那麼 M 也就阻塞了,這個時候,CPU 就需要切換到其他的 M 上執行 G,這個切換的過程也是對於 CPU 資源的浪費

正如人的大腦,同時處理多個事項,當 事項 1 未處理完畢,就去處理事項 2,那麼需要重新理清思緒去執行,若這個時候又需要切換到 事項 1 的時候,就需要花更多的時間來回顧之前事項 1 處理的上下文了, 所以在工作中,沒有特別必要,不要打擾別人

第三, 我們知道在 golang 中,一個 G 也是可以再建立 G 的,就叫 G1 吧,那麼建立的這個 G1 就需要交給其他的 M 來進行執行了,因為當前的 M 正在執行 G,自己忙不過來,因此需要將 G1 交給 M1 執行

這也是一個很明顯的問題

正如上述例子一樣, 2 個人排查相關問題,但是資訊和認知不一致,很明顯是小豬可能是比較難排查出問題來的

就像 M 執行 G 新建立了 G1,但是是交給 M1 來執行的, M1 並不知道 G1 和 G 的關係,M1 執行起來就可能會出現問題,例如需要 G 的一些上下文,但是 M1 並不知情

因此,隨著時間的推移,對於效能的要求是越來越高,當然是要想辦法換一個與時俱進,需要符合時代潮流的管家了,自然就出現了一個新的排程器替代了原有的排程器

出現新的排程器,自然是解決了舊的排程器的缺點,並且還帶來了一些新的屬性和價值,具體的新排程器策略我們可以在下文中進行展示

為什麼會出現協程

在來看另外一個問題,為什麼會出現協程,自然是因為使用程序和執行緒不能夠滿足我們的某些需求了,此處的需求是指對於效能的要求,是對 CPU 利用效率的需求

上圖中我們有說到,對於多程序/多執行緒併發的時候,我們有提高 CPU 的利用率,儘可能的利用好 CPU 的時間片,但是對於 CPU 從 程序/執行緒1 切換到去執行程序/執行緒 2 的過程中,是會產生消耗的,但是這個消耗很難避免

那麼我們知道 CPU 其實執行的是執行緒,那麼如果 1 個執行緒裡面還可以分成多個程式進行併發豈不是可以大大的提高我們當前執行緒的使用效率?

我們從 C/C++ 中知道,咱們的一個32位系統的機器,程序實際上是開闢了一個 4G 的虛擬空間,具體 4G 虛擬空間都包含了什麼,我們可以簡單的看看這個圖,本次不在此細聊

那執行緒大概也是需要 4M 的空間,那麼我們在一定程度上大量的開闢程序或者執行緒,必然會帶來系統中 記憶體佔用高,排程 CPU 切換的消耗高 的問題

後來,我們知道執行緒實際上是分為核心態和使用者態的

當然,使用者態的執行緒是依賴於核心態執行緒的,使用者態中需要執行的內容,是需要放在核心態執行緒上進行執行的,另外, CPU 只知道有核心態執行緒的存在,意味著,CPU 只認核心態執行緒

此處說的核心態,和使用者態,其實就是對應到我們說的 M 和 G

  • 核心態執行緒 -- 執行緒
  • 使用者態執行緒 -- 協程

協程不會陷入到核心態中,因此在 M 不變的情況下,切換 G 就是非常輕快的了,協程簡要有如下特點:

  • 佔用記憶體空間小,只佔用 幾 kb ,比起程序,執行緒來說,真的是很小了
  • 排程靈活,他是處於使用者態進行排程的

根據協程和執行緒處於的使用者態和核心態,我們可以看到排程的機制是不一樣的,

核心態中的執行緒,實際上是搶佔式的,是又 CPU 排程的

使用者態中的執行緒,即協程,是由使用者態排程的,此處的使用者態排程,就可以理解是一個隊列了,只能一個一個的去執行了,一個協程執行完畢之後,讓出 CPU ,才會執行下一個協程

好比我們程式碼中,多個協程,其中 1 個協程 panic 了,如何不捕獲的話,是不是整個程式都崩潰了

對於 CPU 只認核心態執行緒這一點,咱理解起來就想到與一個公司的老闆,只認每個產品線上的總監一樣,你這個主管後面不管有多人少人在幹活,老闆只認為是你這個主管在幹活

當然一個公司也不僅僅只有 1 個老闆,就像計算機系統裡面也會有多個 CPU ,但是道理是一樣的

如何理解 GMP 模型

GMP 分別表示 協程 goroutine,執行緒 thread,處理器 processor

他們三組成了新的排程器,他們三者的關係這樣的

咱們從圖中可以知道有全域性佇列,本地佇列

全域性佇列還是和之前一樣,從裡面裡面 G 的建立,排程,銷燬 都是需要訪問互斥鎖的

P 的本地佇列 自己維護了一個 G 列表,最長是 256 個那 P 的個數一般是 GOMAXPROCS 個,如果本地的佇列滿了,那麼 P 會將佇列中的一半給到全域性佇列中

從本地佇列中取 G 是不需要加鎖的,直接取即可,且如果佇列中的 G 又建立了 G1 ,那麼這個 G1 也是會被優先加入到當前本地佇列的

此處我們可以看到 M 是和 P 進行對應了,當 M 需要執行 G 的時候,就需要先找到一個合適的 P ,從 P 中獲取 G, 如果 P 為空,那麼 M 就會從全域性佇列中獲取一批 G 放到 P 的佇列中

或者是 M 也會從其他的 P 的佇列中偷一半的 G 到當前 P 的佇列中

從這裡我們可以知道,新的排程器,已經解決了舊的排程器的一部分問題了(不需要每次都去找全域性佇列)

那麼 P 是啥時候建立,建立多少個?M 又是啥時候建立的,建立多少個呢? P 和 M 在數量上有必然聯絡嗎?

P 是在程式啟動的時候,讀取環境變數中 GOMAXPROC ,來建立具體 P 的個數

M 的建立實際上是,若 P 中有很多工 G ,如果有空閒的 M,那麼 P 就會找任意空閒 M 來進行處理,如果沒有空閒的 M ,那麼排程器就會去建立 M 來執行 P 中的 任務

那麼新的排程器還有哪些優勢呢?

第一

提高了執行緒的複用率,如果當前執行緒執行完當前 P 的任務之後,當前的 M 會嘗試去偷其他 P 裡面的 G 來進行執行,這樣我們就儘可能的避免了執行緒的建立,銷燬帶來的開銷

第二

如果當前的 M 在執行 G 的時候,出現了阻塞,那麼 M 也會很懂事的讓出當前的 P,讓其他 M 來執行 P 中的任務當前的 M 就繼續處理當前的 G

第三

上述我們說到當前 M 會去從其他 P 的佇列中偷 G,這個是在當前 M 對應的 P 中沒有 G 的時候,優先去做的事情, 如果其他的 P 也沒有 G 的時候,當前 M 才會去全域性佇列中拿

從這裡就可以開始,新的排程器,已經是在大大的弱化了全域性佇列的作用

本次先聊到這裡,相信你對 GMP 的基本理論也有一些瞭解了吧

感謝閱讀,歡迎交流,點個贊,關注一波 再走吧

我正在參與掘金技術社群創作者簽約計劃招募活動,點選連結報名投稿

\