在這篇Go中最強的魔法: 併發程式執行的文章中,我們探討了併發的基本概念以及Go語言中實現併發的機制—Goroutine。這些內容對於剛接觸Go的Gopher來說,可以說是入門級的學習資料。
但是,你可能會好奇,既然Go不是採用傳統的基於執行緒的併發模型,那Go的運行時是如何管理和調度(Schedule)眾多Goroutine的呢?實際上,這些是Go語言核心開發團隊的關注點,普通開發者通常不需要深入了解。不過深入理解Goroutine的調度原理對於寫出高效能的Go程式碼是非常有幫助的。
接下來,讓我們一起簡要探索一下Goroutine調度器的工作原理。
談到「調度」,我們首先會想到作業系統如何管理進程和執行緒。作業系統的調度器會把多個執行緒分配到物理CPU上運行。與此相對應,Goroutine的資源消耗非常小。我們之前提到過,每個Goroutine只需2KB的stack空間。並且,Goroutine的調度切換並不需要陷入作業系統核心,因此成本非常低。這使得一個Go程式可以創建成千上萬的Goroutine來實現併發。
這些Goroutine是如何被調度到CPU上運行的呢?實際上,它們是由一個稱為Goroutine調度器(Goroutine Scheduler)的機制來管理的。但要注意,這裡提到的「CPU」是在引號中,因為從Go程式的視角來看,它與作業系統的交互是有限的。
實際上,對於作業系統而言,一個Go程式只是一個普通的用戶空間(user space)應用程式。作業系統並不直接識別Goroutine,因此所有的Goroutine調度都由Go自己來完成。這就導致了Go runtime必須在其自身內部管理Goroutine之間對「CPU」資源的競爭。
那麼,在Go程式中,Goroutine實際上是在競爭什麼資源呢?答案是作業系統執行緒。因此,Goroutine調度器的工作就是把Goroutine根據一定的算法分配到不同的作業系統執行緒中去執行。
在Go語言的領域中,Goroutine的調度機制、垃圾回收(GC)以及記憶體管理屬於一些較為深奧且複雜的概念。這些主題每一個都可以獨立成章,且隨著Go語言版本的更新,這些主題的實現細節也在不斷進化。本文將嘗試為你揭開基於G-P-M模型的Goroutine調度原理的神秘面紗。如果你對這方面的知識渴望更深入的了解,可以從這裡開始深挖Go的原始碼,探索更多細節。
首先,讓我們來看看G、P、M這三者的定義,它們在Go語言的原始碼中有詳細的闡述,位置在$GOROOT/src/runtime/runtime2.go
。在這個檔案中,你會發現G、P、M這三種結構的定義都非常龐大,每個結構包含了許多程式碼和定義。調度器這塊核心的程式碼相當複雜,考慮到的情況繁多,程式碼之間的「耦合」也十分緊密。不過,通過這些複雜的程式碼,我們還是可以窺見G、P、M各自的主要功能,以下簡要說明:
接下來,我們來討論當Goroutine在沒有進行系統調用、I/O操作或在channel上阻塞時,調度器是如何進行工作的。在這些情況下,Go語言的運行時會利用搶佔式調度來暫停當前Goroutine,並調度其他可運行的Goroutine。一般來說,除了極端情況下的無限迴圈,只要Goroutine執行了函數調用,Go運行時就有機會對其進行搶佔。
Go語言在啟動時會運行一個名為sysmon的M(通常被稱為監控線程),這個特殊的M在Go程式運行的整個過程中發揮著關鍵作用,並且它無需綁定P即可運行(以g0的形式出現)。除此之外,當Goroutine在channel操作或網絡I/O上阻塞,或者在系統調用中阻塞時,Go運行時會有特定的調度策略來處理這些情況,確保整個系統的有效運行。
更多Go語言相關的文章,歡迎參考我的部落格: https://kaichiachen.github.io/2024/02/29/golang/go_goroutine/