弄懂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協程執行的時間也不是絕對公平的。

如有錯誤地方,還請狂噴!