探究 goroutine 併發調度模型

併發,編程裏面最核心的主題,一直以來都是被開發者談及最多的話題;go 語言是通過 goroutine 實現併發編程,goroutine 具有消耗資源低、運行效率高等特點;官方宣稱原生 goroutine 併發成千上萬不成問題。那麼,理解 goroutine 的調度模型和工作原理,對於寫 go 代碼顯得非常重要。

Goroutine 調度器

當一個程序運行時,操作系統會爲程序啓動一個進程,可以把進程看作是一個包含了應用程序運行時需要用到的各種資源的容器;這些資源包括但不限於內存空間、句柄、線程。一個線程是一個執行空間,這個空間會被操作系統調度來執行代碼。對操作系統而言,它的眼裏只有線程,操作系統會在物理處理器上調度線程來運行。

goroutine,是 Go 語言併發編程的實現,是一種用戶層輕量級線程或者說是類協程。Go 程序對操作系統來說只是一個用戶層程序,它甚至不知道 goroutine 的存在。Go 程序本身是運行在一個或多個操作系統線程上,所以 Go 調度器需要將衆多 goroutine 按照一定的算法調度到操作系統的線程上執行。這種在語言層面自帶調度器的,稱之爲原生支持併發

Go 調度器模型

G-P-M 調度模型由 Go 抽象出來的實現,最終形成了 Go 調度器的基本結構:

系統調用阻塞

當正在運行的 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