Go 調度模型 GPM

GPM 模型

[TOC]html

參考: 深刻Golang調度器之GMP模型

前言

在瞭解 Go 的 gorutine 時,咱們仍是得先複習下,併發和並行的區別:程序員

  • 併發:同一段時間執行多個任務(你同時和兩個女友聊天)。
  • 並行:同一時刻執行多個任務(你和你朋友都在和你女友聊天)。

在單核處理器上,經過多線程共享CPU時間片串行執行(併發非並行)。而並行則依賴於多核處理器等物理資源,讓多個任務能夠實現並行執行(併發且並行)。多線程

1、GPM的基本流程

1.1 GPM的含義

  • G,表示一個 goroutine,即我須要分擔出去的任務;
  • P,一個裝滿 G 的隊列,用於維護一些任務;
  • M,一個操做器,用於將一個 G 搬到線程上執行;

1.2 Go調度器基本調度過程

  1. 建立一個 G 對象;
  2. 將 G 保存至 P中;
  3. P 去喚醒(告訴)一個 M,而後繼續執行它的執行序(分配下一個 G);
  4. M 尋找空閒的 P,讀取該 P 要分配的 G;
  5. 接下來 M 執行一個調度循環,調用 G → 執行 → 清理線程 → 繼續找新的 G 執行。

簡單敘述各自的任務併發

  • G,攜帶任務;
  • P,分配任務;
  • M,尋找任務;

2、全面的流程

2.1 各自攜帶的信息

  • G函數

    • 需執行函數的指令(指針)
    • 線程上下文的信息(goroutine切換時,用於保存 g 的上下文,例如,變量、相關信息等)
    • 現場保護和現場恢復(用於全局隊列執行時的保護)
    • 所屬的函數棧
    • 當前執行的 m
    • 被阻塞的時間
  • P,P/M須要進行綁定,構成一個執行單元。P決定了同時能夠併發任務的數量,可經過GOMAXPROCS限制同時執行用戶級任務的操做系統線程。能夠經過runtime.GOMAXPROCS進行指定。高併發

    • 狀態(空閒、運行...)
    • 關聯的 m
    • 可運行的 goroutine 的隊列
    • 下一個 g
  • M,全部M是有線程棧的。若是不對該線程棧提供內存的話,系統會給該線程棧提供內存(不一樣操做系統提供的線程棧大小不一樣)。性能

    • 所屬的調度棧
    • 當前運行的 g
    • 關聯的 p
    • 狀態

以上列舉了三個結構各自的重要屬性,如今咱們來看下詳細的運行流程。操作系統

2.2 準備知識

2.2.1 棧

普通棧:普通棧指的是須要調度的 goroutine 組成的函數棧,是可增加的棧,由於 goroutine 能夠越開越多。線程

線程棧:線程棧是由須要將 goroutine 放置線程上的 m 們組成,實質上 m 也是由 goroutine 生成的,線程棧大小固定(設置了 m 的數量)。全部調度相關的代碼,會先切換到該goroutine的棧中再執行。也就是說線程的棧也是用的g實現,而不是使用的OS的。設計

2.2.2 隊列

全局隊列:該隊列存儲的 G 將被全部的 M 全局共享,爲保證數據競爭問題,需加鎖處理。

本地隊列:該隊列存儲數據資源相同的任務,每一個本地隊列都會綁定一個 M ,指定其完成任務,沒有數據競爭,無需加鎖處理,處理速度遠高於全局隊列。

2.2.3 上下文切換

簡單理解爲當時的環境便可,環境能夠包括當時程序狀態以及變量狀態。

對於代碼中某個值說,上下文是指這個值所在的局部(全局)做用域對象。相對於進程而言,上下文就是進程執行時的環境,具體來講就是各個變量和數據,包括全部的寄存器變量、進程打開的文件、內存(堆棧)信息等。

2.2.4 線程清理

因爲每一個P都須要綁定一個 M 進行任務執行,因此當清理線程的時候,只須要將 P 釋放(解除綁定)(M就沒有任務),便可。P 被釋放主要由兩種狀況:

  • 主動釋放:最典型的例子是,當執行G任務時有系統調用,當發生系統調用時M會處於阻塞狀態。調度器會設置一個超時時間,當超時時會將P釋放。
  • 被動釋放:若是發生系統調用,有一個專門監控程序,進行掃描當前處於阻塞的P/M組合。當超過系統程序設置的超時時間,會自動將P資源搶走。去執行隊列的其它G任務。
阻塞是正在運行的線程沒有運行結束,暫時讓出 CPU。

2.2.5 搶佔式調度

runtime.main中會建立一個額外m運行sysmon函數,搶佔就是在sysmon中實現的。

sysmon會進入一個無限循環, 第一輪迴休眠20us, 以後每次休眠時間倍增, 最終每一輪都會休眠10ms. sysmon中有netpool(獲取fd事件), retake(搶佔), forcegc(按時間強制執行gc), scavenge heap(釋放自由列表中多餘的項減小內存佔用)等處理。

搶佔條件

  1. 若是 P 在系統調用中,且時長已通過一次 sysmon 後,則搶佔;

調用 handoffp 解除 M 和 P 的關聯。

  1. 若是 P 在運行,且時長通過一次 sysmon 後,而且時長超過設置的阻塞時長,則搶佔;

設置標識,標識該函數能夠被停止,當調用棧識別到這個標識時,就知道這是搶佔觸發的, 這時會再檢查一遍是否要搶佔。

2.3 詳細流程

基本流程和上面同樣。每建立出一個 g,優先建立一個 p 進行存儲,當 p 達到限制後,則加入狀態爲 waiting 的隊列中。

若是 g 執行時須要被阻塞,則會進行上下文切換,系統歸還資源後,再返回繼續執行。

當一個G長久阻塞在一個M上時,runtime會新建一個M,阻塞G所在的P會把其餘的G 掛載在新建的M上。當舊的G阻塞完成或者認爲其已經死掉時 回收舊的M(搶佔式調度)。

P會對本身管理的goroutine隊列作一些調度(好比把佔用CPU時間較長的goroutine暫停、運行後續的goroutine等等)當本身的隊列消費完了就去全局隊列裏取,若是全局隊列裏也消費完了會去其餘P的隊列裏搶任務(因此須要單獨存儲下一個 g 的地址,而不是從隊列裏獲取)。

3、總結

相比大多數並行設計模型,Go比較優點的設計就是P上下文這個概念的出現,若是隻有G和M的對應關係,那麼當G阻塞在IO上的時候,M是沒有實際在工做的,這樣形成了資源的浪費,沒有了P,那麼全部G的列表都放在全局,這樣致使臨界區太大,對多核調度形成極大影響。

而goroutine在使用上面的特色,感受既能夠用來作密集的多核計算,又能夠作高併發的IO應用,作IO應用的時候,寫起來感受和對程序員最友好的同步阻塞同樣,而實際上因爲runtime的調度,底層是以同步非阻塞的方式在運行(即IO多路複用)。

因此說保護現場的搶佔式調度和G被阻塞後傳遞給其餘m調用的核心思想,使得goroutine的產生。

單從線程調度講,Go語言相比起其餘語言的優點在於OS線程是由OS內核來調度的,goroutine則是由Go運行時(runtime)本身的調度器調度的,這個調度器使用一個稱爲m:n調度的技術(複用/調度m個goroutine到n個OS線程)。 其一大特色是goroutine的調度是在用戶態下完成的, 不涉及內核態與用戶態之間的頻繁切換,包括內存的分配與釋放,都是在用戶態維護着一塊大的內存池, 不直接調用系統的malloc函數(除非內存池須要改變),成本比調度OS線程低不少。 另外一方面充分利用了多核的硬件資源,近似的把若干goroutine均分在物理線程上, 再加上自己goroutine的超輕量,以上種種保證了go調度方面的性能。

相關文章
相關標籤/搜索