Golang GMP模型
GMP 是 Go 語言運行時(runtime)中的一個重要組件,它是 Go 語言的調度模型。GMP 模型使用三種不同的線程來處理 Go 程序:Goroutine、M(Machine)和 P(Processor)。在 GMP 模型中,Goroutine 是實際編寫的程序代碼,M 是 Go 語言的內部線程,P 則負責管理 M 和 Goroutine 之間的調度。
解釋一下 GMP 模型的工作原理:
GMP 模型的核心是三個概念:G、M 和 P。
-
G:Goroutine是Go語言中的輕量級協程,它可以被看作是一種用戶態的線程,能夠在單個線程上模擬出多個線程的併發執行效果。Goroutine的調度由M和P共同協作完成。
-
M:Machine是操作系統線程的簡稱,它是Go語言實現協程調度的實際執行者。每個M都有一個Goroutine隊列,以及一個P調度器,用於管理並調度Goroutine。M的數量可以通過GOMAXPROCS環境變量進行配置,默認爲CPU核心數。
-
P:Processor是調度器的簡稱,它負責將Goroutine綁定到M線程上,並將Goroutine放入隊列中,等待M線程運行。每個P都有一個Goroutine隊列,以及一個M調度器,用於管理並調度M線程。P的數量是由Go運行時自動根據需要進行調整的。
GMP模型的本質是將一組Goroutine分配到一組M線程中,通過P調度器來管理並調度它們的執行。當一個Goroutine被創建時,它會首先被放入到P的隊列中,等待M線程來執行。M線程會從P的隊列中取出一個Goroutine,並將其執行,如果執行過程中發生阻塞,M線程會放棄該Goroutine的執行,並將其重新放回到P的隊列中,等待後續的調度。
在GMP模型中,調度器P有兩個重要的任務,一是負責調度M線程的執行,二是負責調度Goroutine的執行。P會通過兩個隊列來分別管理M線程和Goroutine,M線程隊列中存儲等待調度的M線程,Goroutine隊列中存儲等待調度的Goroutine。P會按照一定的調度算法,從M線程隊列中選取一個M線程,並將其綁定到一個Goroutine上執行。
具體來說,Goroutine 是 Go 語言中的併發基本單元,一個 Goroutine 類似於一條輕量級的線程,它由 Go 運行時負責調度和管理。Goroutine 是 Go 語言的一大特色,因爲它具有極低的啓動和銷燬成本,一個 Go 程序可以同時創建數百萬個 Goroutine,而這些 Goroutine 的調度則由 Go 運行時自動進行,無需程序員手動管理。
M 是 Go 運行時對操作系統線程的抽象,每個 M 負責管理一條操作系統線程以及其中運行的 Goroutine。每個 M 與一個或多個 P 相關聯,P 是 Goroutine 調度器,它負責在 M 上運行 Goroutine,並將其調度到其他 M 上執行。每個 P 都有一個隊列,用於存放可運行的 Goroutine。
P 由調度器創建,並且與 M 綁定。P 的數量可以通過 GOMAXPROCS 環境變量設置。當一個 Goroutine 準備就緒後,調度器會將其加入到某個 P 的隊列中,P 就會負責運行這個 Goroutine,直到其執行完畢或者被阻塞。
總的來說,GMP 模型實現了一個三層的調度器,其中最上層是 Goroutine 調度器,用於調度 Goroutine;第二層是 P 調度器,用於將可運行的 Goroutine 調度到某個 P 上執行;第三層是 M 調度器,用於將 P 調度到某個 M 上運行。
在 GMP 模型中,調度器的核心是 Work-stealing 調度算法。這個算法可以保證 Goroutine 在不同 P 上的負載均衡,並且能夠充分利用多核 CPU 的性能。
在實踐中,GMP 模型讓 Go 語言具有了出色的併發性能和擴展性。程序員只需要編寫簡單的 Goroutine 代碼,就可以充分利用多核 CPU 併發執行任務。
總的來說,GMP模型通過將Goroutine分配到一組M線程中,並由P調度器管理和調度,實現了高效的協程調度。Goroutine通過輕量級的棧和調度器管理,大大提高了程序的併發能力和執行效率。
GMP 模型的設計目標是充分利用多核 CPU,提高併發程序的性能。具體來說,GMP 模型可以通過以下幾種方式實現:
- 多線程執行:GMP 模型中的 M 可以同時運行多個 Goroutine,充分利用多核 CPU 的計算能力。
- 多態調度:P 可以將 Goroutine 分配給任意一個 M,實現了多態調度,充分利用了計算資源。
- 伸縮性:GMP 模型可以根據系統的負載情況動態調整 M 和 P 的數量,實現伸縮性。
下面是一個簡單的 GMP 模型的例子,演示瞭如何使用 GMP 模型實現併發處理任務:
package main
import (
"fmt"
"runtime"
"sync"
)
func main() {
runtime.GOMAXPROCS(1) // 設置使用一個處理器核心
var wg sync.WaitGroup // 定義一個同步等待組
wg.Add(2) // 有兩個任務需要完成
fmt.Println("Start Goroutines")
go func() { // 啓動一個新的 goroutine
defer wg.Done() // 在函數退出時調用 wg.Done(),通知等待組一個任務已完成
for count := 0; count < 3; count++ {
for char := 'a'; char < 'a'+26; char++ {
fmt.Printf("%c ", char)
}
fmt.Println()
}
}()
go func() { // 啓動另一個 goroutine
defer wg.Done() // 在函數退出時調用 wg.Done(),通知等待組一個任務已完成
for count := 0; count < 3; count++ {
for char := 'A'; char < 'A'+26; char++ {
fmt.Printf("%c ", char)
}
fmt.Println()
}
}()
fmt.Println("Waiting To Finish")
wg.Wait() // 等待所有 goroutine 完成
fmt.Println("\nTerminating Program")
}
Work-stealing 調度算法
package main
import (
"fmt"
"runtime"
"sync"
"time"
)
func main() {
numCPUs := runtime.NumCPU() // 獲取可用處理器核心數
runtime.GOMAXPROCS(numCPUs) // 設置使用所有處理器核心
work := make(chan int, numCPUs) // 創建一個帶有緩衝的int類型通道
var wg sync.WaitGroup // 定義一個同步等待組
for i := 0; i < numCPUs; i++ {
wg.Add(1) // 爲每個處理器核心增加一個任務
go func() { // 啓動每個處理器核心
defer wg.Done()
for {
select {
case job, ok := <-work: // 從通道中獲取工作
if ok {
fmt.Println("Worker", i, "processing job", job)
time.Sleep(time.Second) // 模擬工作時間
} else {
fmt.Println("Worker", i, "shutting down")
return
}
default: // 如果通道中沒有工作,則尋找其他處理器核心的閒置工作
runtime.Gosched()
}
}
}()
}
for i := 0; i < numCPUs*2; i++ { // 向通道中添加工作
work <- i
}
close(work) // 關閉通道,表示所有工作都已完成
wg.Wait() // 等待所有處理器核心完成任務
fmt.Println("All work complete")
}
在此示例中,首先使用runtime.NumCPU()獲取可用的處理器核心數,並使用runtime.GOMAXPROCS()將所有處理器核心設置爲可用狀態。創建了一個帶有緩衝的通道,大小爲可用的處理器核心數,以便可以將工作項分發給多個處理器核心。使用sync.WaitGroup來等待所有處理器核心完成任務。在for循環中,爲每個處理器核心啓動一個goroutine,每個goroutine使用select語句從通道中獲取工作。如果通道中有工作,則處理工作,否則通過runtime.Gosched()在處理器核心之間進行工作竊取。在主函數中,向通道中添加工作,並在完成所有工作後關閉通道。最後,調用wg.Wait()等待所有處理器核心完成任務,並打印"All work complete"表示所有工作都已完成。