探究 goroutine 併發調度模型
併發,編程裏面最核心的主題,一直以來都是被開發者談及最多的話題;go 語言是通過 goroutine 實現併發編程,goroutine 具有消耗資源低、運行效率高等特點;官方宣稱原生 goroutine 併發成千上萬不成問題。那麼,理解 goroutine 的調度模型和工作原理,對於寫 go 代碼顯得非常重要。
Goroutine 調度器
當一個程序運行時,操作系統會爲程序啓動一個進程,可以把進程看作是一個包含了應用程序運行時需要用到的各種資源的容器;這些資源包括但不限於內存空間、句柄、線程。一個線程是一個執行空間,這個空間會被操作系統調度來執行代碼。對操作系統而言,它的眼裏只有線程,操作系統會在物理處理器上調度線程來運行。
goroutine,是 Go 語言併發編程的實現,是一種用戶層輕量級線程或者說是類協程。Go 程序對操作系統來說只是一個用戶層程序,它甚至不知道 goroutine 的存在。Go 程序本身是運行在一個或多個操作系統線程上,所以 Go 調度器需要將衆多 goroutine 按照一定的算法調度到操作系統的線程上執行。這種在語言層面自帶調度器的,稱之爲原生支持併發。
Go 調度器模型
-
每個創建的 G(Goroutine)會放到 Go 調度器的全局運行隊列,調度器將隊列中的 goroutine 分配到一個 P 邏輯處理器,並放入邏輯處理器本地的隊列中,等待被執行;
-
每個邏輯處理器 P 需要綁定到 M,M 會啓動一個操作系統線程;
-
粗糙地說調度就是決定何時哪個 goroutine 獲得執行資源、哪個 goroutine 應該停止執行讓出資源、哪個 goroutine 應該被喚醒恢復執行等;
G-P-M 調度模型由 Go 抽象出來的實現,最終形成了 Go 調度器的基本結構:
-
G(Goroutine) ,每個 Goroutine 對應一個 G 結構體,G 存儲 Goroutine 的運行堆棧、狀態以及任務函數,可重用。G 並非執行體,每個 G 需要綁定到 P 才能被調度執行。
-
P(Processor / 邏輯處理器), 對 G 來說,P 邏輯處理器相當於 CPU 核,G 只有綁定到 P 邏輯處理器才能被調度。對 M 來說,P 提供了相關的執行環境 (Context),如內存分配狀態(mcache),任務隊列(G) 等,P 的數量決定了系統內最大可並行的 G 的數量(前提:物理 CPU 核數 >= P 的數量),P 的數量由用戶設置的 GOMAXPROCS 決定,但是不論 GOMAXPROCS 設置爲多大,P 的數量最大爲 256。
-
M(Machine,OS 線程抽象),代表着真正執行計算的資源,在綁定有效的 P 邏輯處理器後,進入調度循環;調度的機制大致是從全局隊列、邏輯處理器 P 的本地隊列以及 wait 隊列中獲取 G,切換到 G 的執行棧上並執行 G 的函數,調用 goexit 做清理工作並回到 M,如此反覆。M 並不保留 G 狀態,這是 G 可以跨 M 調度的基礎,M 的數量是不定的,由 Go Runtime 調整,爲了防止創建過多 OS 線程導致系統調度不過來,目前默認最大限制爲 10000 個。
系統調用阻塞
當正在運行的 goroutine 需要執行一個阻塞的系統調用,如打開 / 讀寫文件;線程和 goroutine 會從邏輯處理器 P 上分離,該線程會繼續阻塞,等待系統調用返回;
此時,邏輯處理器失去了用來運行的線程,調度器(runtime)會創建一個新的線程,並將其綁定到這個邏輯處理器上;
之後,邏輯處理器 P 會從本地隊列選取另一個 goroutine 來運行。一旦阻塞的系統調用執行完成並返回,對應的 goroutine 會放回本地運行隊列,線程會保存好,以便之後繼續使用。
以上是從宏觀的角度對 Goroutine 和基本調度過程進行的一些概要性的總結,Go 的調度有很多複雜的搶佔式調度、阻塞調度的細節,後續再去找相關資料深入理解。
goroutine 是輕量級
棧是用來存儲當前正在運行或掛起的函數的空間,操作系統會爲每個線程分配固定大小(一般是 2MB)的內存塊做棧。2MB 的固定空間,對於小小的 goroutine 是很大的浪費,對於複雜的任務來說又明顯不夠用。
- 動態棧
goroutine 的棧不是固定的,一開始以一個很小的棧空間(2KB)開啓生命週期,棧的大小會根據需要動態伸縮。和操作系統線程的棧有同樣作用,會保存當前正在運行或掛起的函數的本地變量。
- 調度器的切換成本
線程會被操作系統內核調度到處理器上運行,每幾毫秒會發生一次硬件計時器中斷,當前線程需要讓出 CPU 並將線程狀態保存到寄存器中,CPU 繼續處理其他線程任務,在此線程下一次獲得 CPU 執行時間,會從寄存器中恢復該線程上次的的狀態並繼續執行。線程在內核切換上下文是很慢的。
Go 調度器是在其本身運行的用戶層進行調度的,不需要進入內核的上下文切換,調度成本會低很多。
轉自:大黃蜂
zhuanlan.zhihu.com/p/57875135
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/2FZD40BDcHo1FT9Sczmg7w