Go語言的GPM調度器是什麼?

😋我是平也,這有一個專註Gopher技術成長的開源項目「go home」

導讀

相信很多人都聽說過Go語言天然支援高並發,原因是內部有協程(goroutine)加持,可以在一個進程中啟動成千上萬個協程。那麼,它憑什麼做到如此高的並發呢?那就需要先了解什麼是並發模型。

file

並發模型

著名的C++專家Herb Sutter曾經說過「免費的午餐已經終結」。為了讓程式碼運行的更快,單純依靠更快的硬體已經無法得到滿足,我們需要利用多核來挖掘並行的價值,而並發模型的目的就是來告訴你不同執行實體之間是如何協作的。

file

當然,不同的並發模型的協作方式也不盡相同,常見的並發模型有七種:

  • 執行緒與鎖
  • 函數式編程
  • Clojure之道
  • actor
  • 通訊順序進程(CSP)
  • 數據級並行
  • Lambda架構

而今天,我們只講與Go語言相關的並發模型CSP,感興趣的同學可以自行查閱書籍《七周七並發模型》。

file

CSP篇

CSP,全稱Communicating Sequential Processes,意為通訊順序進程,它是七大並發模型中的一種,它的核心觀念是將兩個並發執行的實體通過通道channel連接起來,所有的消息都通過channel傳輸。其實CSP概念早在1978年就被東尼·霍爾提出,由於近來Go語言的興起,CSP又火了起來。

那麼CSP與Go語言有什麼關係呢?接下來我們來看Go語言對CSP並發模型的實現——GPM調度模型。

file

GPM調度模型

GPM代表了三個角色,分別是Goroutine、Processor、Machine。

file

  • Goroutine:就是咱們常用的用go關鍵字創建的執行體,它對應一個結構體g,結構體里保存了goroutine的堆棧資訊
  • Machine:表示作業系統的執行緒
  • Processor:表示處理器,有了它才能建立G、M的聯繫

Goroutine

Goroutine就是程式碼中使用go關鍵詞創建的執行單元,也是大家熟知的有「輕量級執行緒」之稱的協程,協程是不為作業系統所知的,它由程式語言層面實現,上下文切換不需要經過內核態,再加上協程佔用的記憶體空間極小,所以有著非常大的發展潛力。

go func() {}()

在Go語言中,Goroutine由一個名為runtime.go的結構體表示,該結構體非常複雜,有40多個成員變數,主要存儲執行棧、狀態、當前佔用的執行緒、調度相關的數據。還有玩大家很想獲取的goroutine標識,但是很抱歉,官方考慮到Go語言的發展,設置成私有了,不給你調用😏。

type g struct {
	stack struct {
		lo uintptr
		hi uintptr
	} 							// 棧記憶體:[stack.lo, stack.hi)
	stackguard0	uintptr
	stackguard1 uintptr

	_panic       *_panic
	_defer       *_defer
	m            *m				// 當前的 m
	sched        gobuf
	stktopsp     uintptr		// 期望 sp 位於棧頂,用於回溯檢查
	param        unsafe.Pointer // wakeup 喚醒時候傳遞的參數
	atomicstatus uint32
	goid         int64
	preempt      bool       	// 搶佔訊號,stackguard0 = stackpreempt 的副本
	timer        *timer         // 為 time.Sleep 快取的計時器

	...
}

Goroutine調度相關的數據存儲在sched,在協程切換、恢復上下文的時候用到。

type gobuf struct {
	sp   uintptr
	pc   uintptr
	g    guintptr
	ret  sys.Uintreg
	...
}

Machine

M就是對應作業系統的執行緒,最多會有GOMAXPROCS個活躍執行緒能夠正常運行,默認情況下GOMAXPROCS被設置為內核數,假如有四個內核,那麼默認就創建四個執行緒,每一個執行緒對應一個runtime.m結構體。執行緒數等於CPU個數的原因是,每個執行緒分配到一個CPU上就不至於出現執行緒的上下文切換,可以保證系統開銷降到最低。

type m struct {
	g0   *g 
	curg *g
	...
}

M裡面存了兩個比較重要的東西,一個是g0,一個是curg。

  • g0:會深度參與運行時的調度過程,比如goroutine的創建、記憶體分配等
  • curg:代表當前正在執行緒上執行的goroutine。

剛才說P是負責M與G的關聯,所以M裡面還要存儲與P相關的數據。

type m struct {
  ...
	p             puintptr
	nextp         puintptr
	oldp          puintptr
}
  • p:正在運行程式碼的處理器
  • nextp:暫存的處理器
  • old:系統調用之前的執行緒的處理器

Processor

Proccessor負責Machine與Goroutine的連接,它能提供執行緒需要的上下文環境,也能分配G到它應該去的執行緒上執行,有了它,每個G都能得到合理的調用,每個執行緒都不再渾水摸魚,真是居家必備之良品。

file

同樣的,處理器的數量也是默認按照GOMAXPROCS來設置的,與執行緒的數量一一對應。

type p struct {
	m           muintptr

	runqhead uint32
	runqtail uint32
	runq     [256]guintptr
	runnext guintptr
	...
}

結構體P中存儲了性能追蹤、垃圾回收、計時器等相關的欄位外,還存儲了處理器的待運行隊列,隊列中存儲的是待執行的Goroutine列表。

三者的關係

首先,默認啟動四個執行緒四個處理器,然後互相綁定。

file

這個時候,一個Goroutine結構體被創建,在進行函數體地址、參數起始地址、參數長度等資訊以及調度相關屬性更新之後,它就要進到一個處理器的隊列等待發車。

file

啥,又創建了一個G?那就輪流往其他P裡面放唄,相信你排隊取號的時候看到其他窗口沒人排隊也會過去的。

file

假如有很多G,都塞滿了怎麼辦呢?那就不把G塞到處理器的私有隊列里了,而是把它塞到全局隊列里(候車大廳)。

file

除了往裡塞之外,M這邊還要瘋狂往外取,首先去處理器的私有隊列里取G執行,如果取完的話就去全局隊列取,如果全局隊列里也沒有的話,就去其他處理器隊列里偷,哇,這麼饑渴,簡直是惡魔啊!

file

如果哪裡都沒找到要執行的G呢?那M就會因為太失望和P斷開關係,然後去睡覺(idle)了。

file

那如果兩個Goroutine正在通過channel做一些恩恩愛愛的事阻塞住了怎麼辦,難道M要等他們完事了再繼續執行?顯然不會,M並不稀罕這對Go男女,而會轉身去找別的G執行。

file

系統調用

如果G進行了系統調用syscall,M也會跟著進入系統調用狀態,那麼這個P留在這裡就浪費了,怎麼辦呢?這點精妙之處在於,P不會傻傻的等待G和M系統調用完成,而會去找其他比較閑的M執行其他的G。

file

當G完成了系統調用,因為要繼續往下執行,所以必須要再找一個空閑的處理器發車。

file

如果沒有空閑的處理器了,那就只能把G放回全局隊列當中等待分配。

file

sysmon

sysmon是我們的保潔阿姨,它是一個M,又叫監控執行緒,不需要P就可以獨立運行,每20us~10ms會被喚醒一次出來打掃衛生,主要工作就是回收垃圾、回收長時間系統調度阻塞的P、向長時間運行的G發出搶佔調度等等。

詞條解釋

東尼·霍爾

東尼·霍爾,英國電腦科學家,圖靈獎得主,他設計了牛氣衝天的快速排序演算法、霍爾邏輯以及CSP模型。2011年獲頒約翰·馮諾依曼獎。

file


感謝大家的觀看,如果覺得文章對你有所幫助,歡迎關注公眾號「平也」,聚焦Go語言與技術原理。
關注我

Tags: