調度的基礎,模型關係的映射
GPM模型:
- G,Goroutinue
- 被調度器管理的輕量級線程,goroutine使用go關鍵字創建
- 調度系統的最基本單位goroutine,存儲了goroutine的執行stack信息、goroutine狀態以及goroutine的任務函數等。默認的大小是2KB,根據需要逐步上漲。
- G綁定到P上執行
- P,Processor
- 邏輯執行單元
- 存儲了M執行的上下文,包括各種G對象隊列、鏈表、cache和狀態
- G存在於P中的特定鏈表上,同一時刻,P只能在一個M上,因此不需要鎖
- M,Machine
- 操作系統的實際的線程
- OS的執行的單位,Linux下的大小是8MB
- M綁定執行的P,但是不保存P的狀態,因此P可以跨M執行
整體的調度關係
每個M都有一個P,綠色的G表示當前M上運行的G,灰色的表示local G queue
從上圖理解出,P是G實際的運行綁定的單位,因此P的數量決定了可以併發的G數量;又因爲P最終是綁定到M上的,因此M的數量決定了最終的並行的數量。在Golang中,P的數量由runtime.GOMAXPROCS
來控制,M取決於硬件,默認情況下,等於OS的線程數量。
P的隊列空了之後,不會一直閒置,而是會從其它P中或者全局G queue中,如下圖:
關於Global G Queue和每個P的Local G Queue產生方式:
go
關鍵字生成一個G,之後G會嘗試放入當前的P的Local G Queue中,如果失敗了,就放入Global G Queue。
如果G發生阻塞,則會嘗試尋找新的G來運行,阻塞的G返回後重新加入G Queue中。
P在輪詢查找G的時候,每隔61次從Global G Queue中查找,保證Global也可以執行。當一個G執行超過10ms時,schedule會有對應的搶佔機制。
一些底層知識
線程切換與協程切換的區別。LTS(Local Thread Storage)存儲了線程執行需要的堆棧信息,寄存器的數據等,之後線程會load 程序並執行。對於執行中的進程,在對應的地址其實位置,同樣會啓動線程,此時OS會分配對應的內存空間,並啓動執行。Linux系統中,1個線程是啓動的大小8MB,而且啓動和上下文切換會消耗對應的時間。
協程的特點,協程不是OS級別的,因此協程的功能是程序內部調度的。OS感覺不到協程的存在,因爲OS根本就沒有協程的概念!!!
Golang爲協程的代碼段,在堆上分配初始化的2KB的空間,之後進入之前提到的調度流程。一般來說,Golang使用了線程複用的方式,即啓動線程的時候,在上面有協程運行,協程停止或者阻塞的時候,不會主動停止線程,而是更改線程的FS寄存器的值到對應的協程代碼段上,然後此時線程執行的位置就是新的協程的代碼位置了,此時協程切換的代價是改變線程執行的位置,然後執行新的協程,因此代價很小。
這邊可能要後期更正,FS寄存器那邊的概念不是特別清楚
具體調度方案
給出整體的調度狀態切換圖:
sysmon
:搶佔式調度系統,對於執行時間超過10ms的G,會更正爲可搶佔的,其他協程可以搶佔該G的執行,防止被一個G一直佔用。
以下幾種情況會導致Goroutinue阻塞,進而讓出P,使得P與其它G綁定,高效利用CPU:
- syscall:系統調用,比如讀寫長文本等
- Network IO:網絡傳輸數據等
- channel獲取不到數據
- sync包的調用
參考資料:
- https://zboya.github.io/post/go_scheduler/
- https://blog.csdn.net/u010853261/article/details/84790392#Section1_Scheduler_9
- http://www.sizeofvoid.net/goroutine-under-the-hood/