天天看點

Golang(三)Goroutine原理

前言

  • 最近用到了一些 Golang 異步程式設計的地方,感覺 Golang 相對于其他語言(如 Java)對多線程程式設計的支援非常大,使用起來也非常友善。于是決定了解一下 Goroutine 的底層原理。
  • Goroutine 本質是協程,是實作并行計算的核心。隻需要在對應的函數前加上 Go 關鍵詞即可異步執行:
go func() {
}()      

基本概念

  • 并發:一段時間内執行多個程式,即在一個 cpu 上切換着執行多項任務,宏觀上是同時的,微觀上是順序執行
  • 并行:同時執行多個程式,即在多個 cpu 上同時運作不同任務,不需要上下文切換執行不同任務達到宏觀同時進行的效果
  • 程序:程序是計算機中的程式關于某資料集合上的一次運作活動,是系統進行資源配置設定和排程的基本機關。是具有獨立功能的程式關于某個資料集合的一次運作活動。它可以申請和擁有系統資源,是一個動态的概念,是一個活動的實體
  • 線程:線程是程序的一個實體,是 cpu 排程和分派的基本機關,它是比程序更小的能獨立運作的基本機關。線程自己基本上不擁有系統資源,隻擁有一點在運作中必不可少的資源(如程式計數器、一組寄存器和棧),但是它可與同屬一個程序的其他的線程共享程序所擁有的全部資源。線程間通信主要通過共享記憶體,上下文切換較快,資源開銷較少
  • 協程:是一種使用者态的輕量級線程,協程的排程完全由使用者控制。線程和程序的操作是由程式觸發系統接口,最後的執行者是系統;協程的操作執行者則是使用者自身程式。協程擁有自己的寄存器上下文和棧。協程排程切換時,将寄存器上下文和棧儲存到其他地方,在切回來的時候,恢複先前儲存的寄存器上下文和棧,直接操作棧則基本沒有核心切換的開銷,可以不加鎖的通路全局變量,是以上下文的切換非常快。Goroutine 就是一種協程

排程模型

  • Goroutine 并發排程通過 GPM 模型實作,包含四個結構:M、G、P、Sched:
    • M:代表核心級線程,一個 M 對應一個線程
    • G:代表一個 Goroutine,包含自己的棧、程式計數器等資訊
    • P:指處理器,主要用途是用來執行 Goroutine,維護一個 Goroutine 隊列,同時還有一個全局隊列。每一個運作的 M 都必須綁定一個 P,就像線程必須在麼一個 cpu 核上執行一樣
    • Sched:代表排程器,維護 M 和 G 的隊列和狀态資訊
Golang(三)Goroutine原理
  • 如上圖,2 個 M,每個 M 擁有 1 個 P 和 1 個正在運作的 G。
  • P 的數量可以通過 GOMAXPROCS() 設定,代表有多少個 Goroutine 可以同時運作。

調用異步

  • 執行 go func() 時,會在隊列尾部加入一個 Goroutine。
  • 如果此時還有空閑的 P,則建立一個 M。M 會啟動一個底層線程,循環執行能找到的 G 任務。
  • G 任務的執行任務是:先從本地隊列找,本地沒有則從全局隊列找。一次轉移 num(G)/num(P) 個任務,再去其他 P 中擷取隊列一半的任務。

監視逾時

  • 啟動一個 G 時,會專門建立一個 Sysmon,用來監視和管理。記錄所有 P 的 G 任務計數 schedtick(schedtick 會在每執行一個 G 任務後遞增)。
  • 如果檢查到 schedtick 一直沒有遞增,說明這個 P 一直在執行同一個 G 任務,如果超過一定時間(10ms),就在這個 G 任務的棧資訊裡加一個标記。
  • 内聯函數執行時發現标記則中斷自己,把自己加到隊列末尾;非内聯函數則會忽視标記,一直執行到結束。

中斷恢複

  • 對于一個 G 任務中斷後:中斷時将寄存器資訊儲存在 G 對象裡,再次執行時将棧資訊複制到寄存器裡,繼續執行。

首次啟動

  • 系統啟動時主線程啟動,第一個 M1 就是主線程,M1 會綁定一個 P。
  • main 函數作為第一個 Goroutine 執行。
  • main 裡其他的 Goroutine 會綁定到目前 M1 的 P1 上。
  • 執行 main 裡的 Goroutine 時,會建立新的 M2,新 M2 的初始 P2 本地任務隊列時空的,會從 P1 取一些過來。
  • 然後依此類推直到 M 數量達到限制。

參考文獻

  • go語言之行--golang核武器goroutine排程原理、channel詳解
  • 程序、線程、協程之概念了解
  • 程序和線程、協程的差別
  • golang的goroutine排程機制