Golang 的 協程調度機制 與 GOMAXPROCS 性能調優

做者:林冠宏 / 指尖下的幽靈git

掘金:https://juejin.im/user/587f0dfe128fe100570ce2d8github

博客:http://www.cnblogs.com/linguanh/數組

GitHub : https://github.com/af913337456/併發

騰訊雲專欄: https://cloud.tencent.com/developer/user/1148436/activities函數


前序

正確地認識 G , M , P 三者的關係,可以對協程的調度機制有更深刻的理解! 本文將會完整介紹完 go 協程的調度機制,包含:性能

  • 調度對象的主要組成
  • 各對象的關係 與 分工
  • gorutine 協程是如何被執行的
  • 內核線程 sysmon 對 gorutine 的管理
  • gorutine 協程中斷掛起 與 恢復
  • GOMAXPROCS 如何影響 go 的併發性能

目錄

調度器的三個基本對象:

Golang 簡稱 Go,Go 的協程(goroutine) 和咱們常見的線程(Thread)同樣,擁有其調度器。spa

  • G (Goroutine),表明協程,也就是每次代碼中使用 go 關鍵詞時候會建立的一個對象
  • M (Work Thread),工做線程
  • P (Processor),表明一個處理器,又稱上下文

G-M-P三者的關係與特色:

  • 每個運行的 M 都必須綁定一個 P,線程M 建立後會去檢查並執行G (goroutine)對象
  • 每個 P 保存着一個協程G 的隊列
  • 除了每一個 P 自身保存的 G 的隊列外,調度器還擁有一個全局的 G 隊列
  • M 從隊列中提取 G,並執行
  • P 的個數就是GOMAXPROCS(最大256),啓動時固定的,通常不修改
  • M 的個數和 P 的個數不必定同樣多(會有休眠的M 或 P不綁定M )(最大10000)
  • P 是用一個全局數組(255)來保存的,而且維護着一個全局的 P 空閒鏈表

局部G隊列與全局G隊列的關係

  • 全局G任務隊列會和各個本地G任務隊列按照必定的策略互相交換。沒錯,就是協程任務交換
  • G任務的執行順序是,先從本地隊列找,本地沒有則從全局隊列
  • 轉移
    • 局部與全局,全局G個數 / P個數
    • 局部與局部,一次性轉移一半

Gorutine從入隊到執行

  1. 當咱們建立一個G對象,就是 gorutine,它會加入到本地隊列或者全局隊列
  2. 若是還有空閒的P,則建立一個M 綁定該 P ,注意!這裏,P 此前必須還沒綁定過M 的,不然不知足空閒的條件。細節點:
    1. 先找到一個空閒的P,若是沒有則直接返回
    2. P 個數不會佔用超過本身設定的cpu個數
    3. P 在被 M 綁定後,就會初始化本身的 G 隊列,此時是一個空隊列
    4. 注意這裏的一個點
      • 不管在哪一個 M 中建立了一個 G,只要 P 有空閒的,就會引發新 M 的建立
      • 不需考慮當前所在 M 中所綁的 P 的 G 隊列是否已滿
      • 新建立的 M 所綁的 P 的初始化隊列會從其餘 G 隊列中取任務過來
    5. 這裏留下第一個問題:

      若是一個G任務執行時間太長,它就會一直佔用 M 線程,因爲隊列的G任務是順序執行的,其它G任務就會阻塞,如何避免該狀況發生? --①線程

  3. M 會啓動一個底層線程循環執行能找到的 G 任務。這裏的尋找的 G 從下面幾方面找:
    • 當前 M 所綁的 P 隊列中找
    • 去別的 P 的隊列中找
    • 去全局 G 隊列中找
  4. G任務的執行順序是,先從本地隊列找,本地沒有則從全局隊列找
  5. 程序啓動的時候,首先跑的是主線程,而後這個主線程會綁定第一個 P
  6. 入口 main 函數,實際上是做爲一個 goroutine 來執行

解答問題-①

協程的切換時間片是10ms,也就是說 goroutine 最多執行10ms就會被 M 切換到下一個 G。這個過程,又被稱爲 中斷,掛起code

原理:協程

go程序啓動時會首先建立一個特殊的內核線程 sysmon,用來監控和管理,其內部是一個循環:

  1. 記錄全部 P 的 G 任務的計數 schedtick,schedtick會在每執行一個G任務後遞增

  2. 若是檢查到 schedtick 一直沒有遞增,說明這個 P 一直在執行同一個 G 任務,若是超過10ms,就在這個G任務的棧信息裏面加一個 tag 標記

  3. 而後這個 G 任務在執行的時候,若是遇到非內聯函數調用,就會檢查一次這個標記,而後中斷本身,把本身加到隊列末尾,執行下一個G

  4. 若是沒有遇到非內聯函數 調用的話,那就會一直執行這個G任務,直到它本身結束;若是是個死循環,而且 GOMAXPROCS=1 的話。那麼一直只會只有一個 P 與一個 M,且隊列中的其餘 G 不會被執行!

例子,下面的這段代碼,hello world 不會被輸出

func main(){
    runtime.GOMAXPROCS(1)
    go func(){
    	fmt.Println("hello world")
    	// panic("hello world")  // 強制觀察輸出
    }()
    go func(){
    	for {
            // fmt.Println("aaa")  // 非內聯函數,這行註釋打開,將致使 hello world 的輸出
    	}
    }()
    select {}
}

中斷後的恢復

  1. 中斷的時候將寄存器裏的棧信息,保存到本身的 G 對象裏面
  2. 當再次輪到本身執行時,將本身保存的棧信息複製到寄存器裏面,這樣就接着上次以後運

GOMAXPROCS--性能調優

看完上面的內容,相信你已經知道,GOMAXPROCS 就是 go 中 runtime 包的一個函數。它設置了 P 的最多的個數。這也就直接致使了 M 最多的個數是多少,而 M 的個數就決定了各個 G 隊列能同時被多少個 M 線程來進行調取執行!

故,咱們通常將 GOMAXPROCS 的個數設置爲 CPU 的核數,且須要注意的是:

  • go 1.5 版本以前的 GOMAXPROCS 默認是 1
  • go 1.5 版本以後的 GOMAXPROCS 默認是 Num of cpu
相關文章
相關標籤/搜索