Golang併發編程——Goroutine底層實現詳解

線程的分類

線程的實現可以分三類:用戶級線程,內核級線程和混合式線程。

用戶級線程

用戶級線程是指不需要內核支持而在用戶程序中實現的線程,它的內核的切換是由用戶態程序自己控制內核的切換,不需要內核的干涉。但是它不能像內核級線程一樣更好的運用多核CPU。

庫調度器從進程的多個線程中選擇一個線程,然後該線程和該進程允許的一個內核線程關聯起來。內核線程將被操作系統調度器指派到處理器內核。

用戶級線程是一種多對一的線程映射。

內核級線程

內核級線程:切換由內核控制,當線程進行切換的時候,由用戶態轉化爲內核態。切換完畢要從內核態返回用戶態。可以很好的運用多核CPU,就像Windows電腦的四核八線程,雙核四線程一樣。

內核線程駐留在內核空間,它們是內核對象。有了內核線程,每個用戶線程被映射或綁定到一個內核線程。用戶線程在其生命期內都會綁定到該內核線程。一旦用戶線程終止,兩個線程都將離開系統。

這被稱作一對一線程映射。

goroutine是什麼?

通常goroutine會被當做coroutine(協程)的 golang實現。
goroutine和它的Go Scheduler在底層實現上其實是屬於混合型線程,goroutine並不等同於協程。

組合方式的多線程實現, 線程創建完全在用戶空間中完成,線程的調度和同步也在應用程序中進行. 一個應用程序中的多個用戶級線程被映射到一些(小於或等於用戶級線程的數目)內核級線程上。

下圖說明了用戶級與內核級的組合實現方式, 在這種模型中,每個內核級線程有一個可以輪流使用的用戶級線程集合

Goroutine的內存分配

每一個OS線程都有一個固定大小的內存塊(一般會是2MB)來做棧,這個棧會用來存儲當前正在被調用或掛起(指在調用其它函數時)的函數的內部變量。這個固定大小的棧同時很大又很小。因爲2MB的棧對於一個小小的goroutine來說是很大的內存浪費,而對於一些複雜的任務(如深度嵌套的遞歸)來說又顯得太小。因此,Go語言做了它自己的『線程』。

在Go語言中,每一個goroutine是一個獨立的執行單元,相較於每個OS線程固定分配2M內存的模式,goroutine的棧採取了動態擴容方式, 初始時僅爲2KB,隨着任務執行按需增長,最大可達1GB,且完全由golang自己的調度器 Go Scheduler 來調度。此外,GC還會週期性地將不再使用的內存回收,收縮棧空間。

G-P-M 模型

  • G: 表示Goroutine,每個Goroutine對應一個G結構體,G存儲Goroutine的運行堆棧、狀態以及任務函數,可重用。G並非執行體,每個G需要綁定到P才能被調度執行。
  • P: Processor,表示邏輯處理器, 對G來說,P相當於CPU核,G只有綁定到P(在P的local runq中)才能被調度。對M來說,P提供了相關的執行環境(Context),如內存分配狀態(mcache),任務隊列(G)等,P的數量決定了系統內最大可並行的G的數量(前提:物理CPU核數 >= P的數量),P的數量由用戶設置的GOMAXPROCS決定,但是不論GOMAXPROCS設置爲多大,P的數量最大爲256。
  • M: Machine,OS線程抽象,代表着真正執行計算的資源,在綁定有效的P後,進入schedule循環;而schedule循環的機制大致是從Global隊列、P的Local隊列以及wait隊列中獲取G,切換到G的執行棧上並執行G的函數,調用goexit做清理工作並回到M,如此反覆。M並不保留G狀態,這是G可以跨M調度的基礎,M的數量是不定的,由Go Runtime調整,爲了防止創建過多OS線程導致系統調度不過來,目前默認最大限制爲10000個。

Go調度器工作時會維護兩種用來保存G的任務隊列:一種是一個Global任務隊列,一種是每個P維護的Local任務隊列。

當通過go關鍵字創建一個新的goroutine的時候,它會優先被放入P的本地隊列。爲了運行goroutine,M需要持有(綁定)一個P,接着M會啓動一個OS線程,循環從P的本地隊列裏取出一個goroutine並執行。還有work-stealing調度算法:當M執行完了當前P的Local隊列裏的所有G後,P也不會就這麼在那躺屍啥都不幹,它會先嚐試從Global隊列尋找G來執行,如果Global隊列爲空,它會隨機挑選另外一個P,從它的隊列裏中拿走一半的G到自己的隊列中執行。

進階閱讀開源Golang線程池

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章