弄懂goroutine調度原理
- 2019 年 10 月 3 日
- 筆記
goroutine簡介
golang語言作者Rob Pike說,「Goroutine是一個與其他goroutines 並發運行在同一地址空間的Go函數或方法。一個運行的程式由一個或更多個goroutine組成。它與執行緒、協程、進程等不同。它是一個goroutine「。
- goroutine通過通道來通訊,而協程通過讓出和恢復操作來通訊;
- goroutine 通過Golang 的調度器進行調度,而協程通過程式本身調度;
簡單的說就是Golang自己實現了協程並叫做goruntine(本文稱Go協程),且比協程更強大。
goroutine調度原理
上面說到Go協程是通過Golang的調度器進行調度的,其中調度器的執行緒模型為兩級執行緒模型。
有關兩級執行緒模型的介紹,可以看這篇文章
我們來看下Golang實現的兩級執行緒模型是怎樣的。首先要知道這三個字母代表的含義
- M:代表內核級的執行緒
- P:全程Processor,代表運行Go協程所需要的資源(上下文環境)
- G:代表Go協程
我們先看下為實現調度Golang定義了這些數據結構存M,P,G
名稱 | 作用範圍 | 描述 |
---|---|---|
全局M列表 | Go的運行時 | 存放所有M的單向鏈表 |
全局P列表 | Go的運行時 | 存放所有P的數組 |
全局G列表 | Go的運行時 | 存放所有G的切片 |
調度器的空閑M列表 | 調度器 | 存放空閑M的單向鏈表 |
調度器的空閑P列表 | 調度器 | 存放空閑P的單向鏈表 |
調度器的自由G列表 | 調度器 | 存放自由G的單向鏈表(有兩個) |
調度器的可運行G隊列 | 調度器 | 存放可運行G的隊列 |
P的自由G列表 | 本地P | 存放當前P中自由G的單向鏈表 |
P的可運行G隊列 | 本地P | 存放當前P中可運行G的隊列 |
然後從上往下解析Go的兩級執行緒模型圖
(1)M和內核執行緒之間是一對一的關係,一個M在其生命周期中,只會和一個內核執行緒關聯,所以不會出現對內核執行緒的頻繁切換;
Golang的運行時執行系統監控和垃圾回收等任務時候會導致創建M,M空閑時不會被銷毀,而是放到一個
調度器的空閑M列表
中,等待與P關聯,M默認數量為10000
(2)P和M之間是多對多的關係,P和G之間是一對多的關係,他們的關聯是易變的,由Golang的調度器完成調度;
Golang的運行時按規則調度,讓P和不同的M建立或斷開關聯,使得P中的G能夠及時獲得運行時機
(3)P的數量默認為CPU總核心數,最大為256,當P沒有可運行的G時候(P的可運行G隊列為空),P會被放到調度器的空閑P列表
中,等待M與它關聯;
P有可能會被銷毀,如運行時用runtime.GOMAXPROCS把P的數量從32降到16時,剩餘16個會被銷毀,它們原來的G會先轉到調度器
可運行的G隊列
和自由G列表
(4)每個P中有可運行的G隊列
(如圖中最下面的那行G)和自由G列表
(圖中未畫出來),當G的程式碼執行完後,該G不會被銷毀,而是被放到P的自由G列表
或調度器的自由G列表
。如果程式新建了Go協程,調度器會在自由G列表中取一個G,然後把Go協程的函數賦值到G中(如果自由G列表為空,就創建一個G);
可見Golang調度器在調度時很大程度復用了M,P,G
(5)在Go程式初始化後,調度器首先進行一輪調度,此時用M去搜索可運行的G。其中我們的main函數也是一個G,找到可運行的G後就執行它;
至於怎麼找可運行的G呢?答案是到處找,想盡辦法找(這裡只列出一部分地方)。
- 從
本地P的可運行的G隊列
找- 從
調度器的可運行的G隊列
找- 從
其他P的可運行的G隊列
找
(6)P的可運行G隊列
最大只能存放長度為256的G,當隊列滿後,調度器會把一半的G轉到調度器的可運行G隊列
。
系統監控
上面大概描述了關於goroutine調度的流程。現在還存在一個問題,那就是當Go協程很多(並發量大)時候,顯然G是不能一直執行下去的,因為也需要把執行機會留給其他的G。此時Golang運行時的系統監控就起作用了。
一般情況,當G運行時間超過10ms後,該G就會被系統告知需要停止了,讓其他G運行。(這裡情況比較複雜,並不能確保每個G都能被公平執行)
以下特殊情況該G不需要停止
- P的可運行G隊列為空(沒有其他G可運行)
- 有空閑的M在尋找可運行的G(沒有其他G可運行)
- 空閑的P(還有P閑著)
總結
Golang以兩級執行緒實現模型,自己實現goruntine和調度器,優勢在於並行和非常低的資源使用。
主要體現:
- 記憶體消耗方面(每個Go協程占的記憶體遠小於執行緒占的記憶體)
- 切換(調度)開銷方面
- 執行緒切換涉及模式切換(從用戶態切換到內核態)
此外,Go協程執行任務完成的順序並不都是按我們預期的那樣(程式不加以控制的情況下),特別在一些耗時較長的任務中。且每個Go協程執行的時間也不是絕對公平的。
如有錯誤地方,還請狂噴!