本文原文地址:GoLang協(xié)程Goroutiney原理與GMP模型詳解
什么是goroutine
Goroutine是Go語(yǔ)言中的一種輕量級(jí)線程,也成為協(xié)程,由Go運(yùn)行時(shí)管理。它是Go語(yǔ)言并發(fā)編程的核心概念之一。Goroutine的設(shè)計(jì)使得在Go中實(shí)現(xiàn)并發(fā)編程變得非常簡(jiǎn)單和高效。
以下是一些關(guān)于Goroutine的關(guān)鍵特性:
- 輕量級(jí):Goroutine的創(chuàng)建和切換開(kāi)銷非常小。與操作系統(tǒng)級(jí)別的線程相比,Goroutine占用的內(nèi)存和資源更少。一個(gè)典型的Goroutine只需要幾KB的棧空間,并且??臻g可以根據(jù)需要?jiǎng)討B(tài)增長(zhǎng)。
- 并發(fā)執(zhí)行:Goroutine可以并發(fā)執(zhí)行多個(gè)任務(wù)。Go運(yùn)行時(shí)會(huì)自動(dòng)將Goroutine調(diào)度到可用的處理器上執(zhí)行,從而充分利用多核處理器的能力。
- 簡(jiǎn)單的語(yǔ)法:?jiǎn)?dòng)一個(gè)Goroutine非常簡(jiǎn)單,只需要在函數(shù)調(diào)用前加上go關(guān)鍵字。例如,go myFunction()會(huì)啟動(dòng)一個(gè)新的Goroutine來(lái)執(zhí)行myFunction函數(shù)。
- 通信和同步:Go語(yǔ)言提供了通道(Channel)機(jī)制,用于在Goroutine之間進(jìn)行通信和同步。通道是一種類型安全的通信方式,可以在不同的Goroutine之間傳遞數(shù)據(jù)。
什么是協(xié)程
協(xié)程(Coroutine)是一種比線程更輕量級(jí)的并發(fā)編程方式。它允許在單個(gè)線程內(nèi)執(zhí)行多個(gè)任務(wù),并且可以在任務(wù)之間進(jìn)行切換,而不需要進(jìn)行線程上下文切換的開(kāi)銷。協(xié)程通過(guò)協(xié)作式多任務(wù)處理來(lái)實(shí)現(xiàn)并發(fā),這意味著任務(wù)之間的切換是由程序顯式控制的,而不是由操作系統(tǒng)調(diào)度的。
以下是協(xié)程的一些關(guān)鍵特性:
- 輕量級(jí):協(xié)程的創(chuàng)建和切換開(kāi)銷非常小,因?yàn)樗鼈儾恍枰僮飨到y(tǒng)級(jí)別的線程管理。
- 非搶占式:協(xié)程的切換是顯式的,由程序員在代碼中指定,而不是由操作系統(tǒng)搶占式地調(diào)度。
- 狀態(tài)保存:協(xié)程可以在暫停執(zhí)行時(shí)保存其狀態(tài),并在恢復(fù)執(zhí)行時(shí)繼續(xù)從暫停的地方開(kāi)始。
- 異步編程:協(xié)程非常適合用于異步編程,特別是在I/O密集型任務(wù)中,可以在等待I/O操作完成時(shí)切換到其他任務(wù),從而提高程序的并發(fā)性和效率。
Goroutin就是Go在協(xié)程這個(gè)場(chǎng)景上的實(shí)現(xiàn)。
以下是一個(gè)簡(jiǎn)單的go goroutine例子,展示了如何使用協(xié)程:
package main
import (
"fmt"
"sync"
"time"
)
// 定義一個(gè)簡(jiǎn)單的函數(shù),模擬一個(gè)耗時(shí)操作
func printNumbers(wg *sync.WaitGroup) {
defer wg.Done() // 在函數(shù)結(jié)束時(shí)調(diào)用Done方法
for i := 1; i <= 5; i++ {
fmt.Printf("Number: %d\n", i)
time.Sleep(1 * time.Second) // 模擬耗時(shí)操作
}
}
func main() {
var wg sync.WaitGroup
// 啟動(dòng)一個(gè)goroutine來(lái)執(zhí)行printNumbers函數(shù)
wg.Add(1)
go printNumbers(&wg)
// 主goroutine繼續(xù)執(zhí)行其他操作
for i := 'A'; i <= 'E'; i++ {
fmt.Printf("Letter: %c\n", i)
time.Sleep(1 * time.Second) // 模擬耗時(shí)操作
}
// 等待所有g(shù)oroutine完成
wg.Wait()
}
我們定義了一個(gè)名為printNumbers的函數(shù),該函數(shù)會(huì)打印數(shù)字1到5,并在每次打印后暫停1秒。然后,在main函數(shù)中,我們使用go關(guān)鍵字啟動(dòng)一個(gè)新的goroutine來(lái)執(zhí)行printNumbers函數(shù)。同時(shí),主goroutine繼續(xù)執(zhí)行其他操作,打印字母A到E,并在每次打印后暫停1秒。
需要注意的是,主goroutine和新啟動(dòng)的goroutine是并發(fā)執(zhí)行的。為了確保所有g(shù)oroutine完成,我們使用sync.WaitGroup來(lái)等待所有g(shù)oroutine完成。我們?cè)趩?dòng)goroutine之前調(diào)用wg.Add(1),并在printNumbers函數(shù)結(jié)束時(shí)調(diào)用wg.Done()。最后,我們?cè)趍ain函數(shù)中調(diào)用wg.Wait(),等待所有g(shù)oroutine完成。這樣可以確保程序在所有g(shù)oroutine完成之前不會(huì)退出。
協(xié)程是一種強(qiáng)大的工具,可以簡(jiǎn)化并發(fā)編程,特別是在處理I/O密集型任務(wù)時(shí)。
Goroutin實(shí)現(xiàn)原理
Goroutine的實(shí)現(xiàn)原理包括Goroutine的創(chuàng)建、調(diào)度、上下文切換和棧管理等多個(gè)方面。通過(guò)GPM模型和高效的調(diào)度機(jī)制,Go運(yùn)行時(shí)能夠高效地管理和調(diào)度大量的Goroutine,實(shí)現(xiàn)高并發(fā)編程。
Goroutine的創(chuàng)建
當(dāng)使用go關(guān)鍵字啟動(dòng)一個(gè)新的Goroutine時(shí),Go運(yùn)行時(shí)會(huì)執(zhí)行以下步驟:
- 分配G結(jié)構(gòu)體:Go運(yùn)行時(shí)會(huì)為新的Goroutine分配一個(gè)G結(jié)構(gòu)體(G表示Goroutine),其中包含Goroutine的狀態(tài)信息、棧指針、程序計(jì)數(shù)器等。
- 分配棧空間:Go運(yùn)行時(shí)會(huì)為新的Goroutine分配初始的??臻g,通常是幾KB。這個(gè)??臻g是動(dòng)態(tài)增長(zhǎng)的,可以根據(jù)需要自動(dòng)擴(kuò)展。
- 初始化G結(jié)構(gòu)體:Go運(yùn)行時(shí)會(huì)初始化G結(jié)構(gòu)體,將Goroutine的入口函數(shù)、參數(shù)、棧指針等信息填入G結(jié)構(gòu)體中。
- 將Goroutine加入調(diào)度隊(duì)列:Go運(yùn)行時(shí)會(huì)將新的Goroutine加入到某個(gè)P(Processor)的本地運(yùn)行隊(duì)列中,等待調(diào)度執(zhí)行。
Goroutine的調(diào)度
Go運(yùn)行時(shí)使用GPM模型(Goroutine、Processor、Machine)來(lái)管理和調(diào)度Goroutine。調(diào)度過(guò)程如下:
- P(Processor):P是Go運(yùn)行時(shí)的一個(gè)抽象概念,表示一個(gè)邏輯處理器。每個(gè)P持有一個(gè)本地運(yùn)行隊(duì)列,用于存儲(chǔ)待執(zhí)行的Goroutine。P的數(shù)量通常等于機(jī)器的CPU核心數(shù),可以通過(guò)runtime.GOMAXPROCS函數(shù)設(shè)置。
- M(Machine):M表示一個(gè)操作系統(tǒng)線程。M負(fù)責(zé)實(shí)際執(zhí)行P中的Goroutine。M與P是一對(duì)一綁定的關(guān)系,一個(gè)M只能綁定一個(gè)P,但一個(gè)P可以被多個(gè)M綁定(通過(guò)搶占機(jī)制)。M的數(shù)量是由Go運(yùn)行時(shí)系統(tǒng)動(dòng)態(tài)管理和確定的。M的數(shù)量并不是固定的,而是根據(jù)程序的運(yùn)行情況和系統(tǒng)資源的使用情況動(dòng)態(tài)調(diào)整的。通過(guò)runtime.NumGoroutine()和runtime.NumCPU()函數(shù),我們可以查看當(dāng)前的Goroutine數(shù)量和CPU核心數(shù)。Go運(yùn)行時(shí)對(duì)M的數(shù)量有一個(gè)默認(rèn)的最大限制,以防止創(chuàng)建過(guò)多的M導(dǎo)致系統(tǒng)資源耗盡。這個(gè)限制可以通過(guò)環(huán)境變量GOMAXPROCS進(jìn)行調(diào)整,但通常不需要手動(dòng)設(shè)置。
- G(Goroutine):代表一個(gè)goroutine,它有自己的棧,instruction pointer和其他信息(正在等待的channel等等),用于調(diào)度。
-
調(diào)度循環(huán):每個(gè)P會(huì)在一個(gè)循環(huán)中不斷從本地運(yùn)行隊(duì)列中取出Goroutine,并將其分配給綁定的M執(zhí)行。如果P的本地運(yùn)行隊(duì)列為空,P會(huì)嘗試從其他P的本地運(yùn)行隊(duì)列中竊取Goroutine(工作竊取機(jī)制)。
image.png
從上圖中看,有2個(gè)物理線程M,每一個(gè)M都擁有一個(gè)處理器P,每一個(gè)也都有一個(gè)正在運(yùn)行的goroutine。P的數(shù)量可以通過(guò)GOMAXPROCS()來(lái)設(shè)置,它其實(shí)也就代表了真正的并發(fā)度,即有多少個(gè)goroutine可以同時(shí)運(yùn)行。圖中灰色的那些goroutine并沒(méi)有運(yùn)行,而是出于ready的就緒態(tài),正在等待被調(diào)度。P維護(hù)著這個(gè)隊(duì)列(稱之為runqueue),Go語(yǔ)言里,啟動(dòng)一個(gè)goroutine很容易:go function 就行,所以每有一個(gè)go語(yǔ)句被執(zhí)行,runqueue隊(duì)列就在其末尾加入一個(gè)goroutine,在下一個(gè)調(diào)度點(diǎn),就從runqueue中取出(如何決定取哪個(gè)goroutine?)一個(gè)goroutine執(zhí)行。
P的數(shù)量可以大于器的CPU核心數(shù)?
在Go語(yǔ)言中,P(Processor)的數(shù)量通常等于機(jī)器的CPU核心數(shù),但也可以通過(guò)runtime.GOMAXPROCS函數(shù)進(jìn)行調(diào)整。默認(rèn)情況下,Go運(yùn)行時(shí)會(huì)將P的數(shù)量設(shè)置為機(jī)器的邏輯CPU核心數(shù)。然而,P的數(shù)量可以被設(shè)置為大于或小于機(jī)器的CPU核心數(shù),這取決于具體的應(yīng)用需求和性能考慮。
調(diào)整P的數(shù)量,可以使用runtime.GOMAXPROCS函數(shù)來(lái)設(shè)置P的數(shù)量。例如:
package main
import (
"fmt"
"runtime"
"sync"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("Worker %d starting\n", id)
// 模擬工作負(fù)載
for i := 0; i < 1000000000; i++ {
}
fmt.Printf("Worker %d done\n", id)
}
func main() {
// 設(shè)置P的數(shù)量為機(jī)器邏輯CPU核心數(shù)的兩倍
numCPU := runtime.NumCPU()
runtime.GOMAXPROCS(numCPU * 2)
var wg sync.WaitGroup
// 啟動(dòng)多個(gè)Goroutine
for i := 1; i <= 10; i++ {
wg.Add(1)
go worker(i, &wg)
}
// 等待所有Goroutine完成
wg.Wait()
fmt.Println("All workers done")
}
在這個(gè)示例中,我們將P的數(shù)量設(shè)置為機(jī)器邏輯CPU核心數(shù)的兩倍。這樣做的目的是為了觀察在不同P數(shù)量設(shè)置下程序的性能表現(xiàn)。
- P的數(shù)量大于CPU核心數(shù)的影響
- 上下文切換增加:當(dāng)P的數(shù)量大于CPU核心數(shù)時(shí),可能會(huì)導(dǎo)致更多的上下文切換。因?yàn)椴僮飨到y(tǒng)需要在有限的CPU核心上調(diào)度更多的線程(M),這可能會(huì)增加調(diào)度開(kāi)銷。
- 資源競(jìng)爭(zhēng):更多的P意味著更多的Goroutine可以同時(shí)運(yùn)行,但這也可能導(dǎo)致更多的資源競(jìng)爭(zhēng),特別是在I/O密集型任務(wù)中。過(guò)多的P可能會(huì)導(dǎo)致資源爭(zhēng)用,反而降低程序的整體性能。
- 并發(fā)性提高:在某些情況下,增加P的數(shù)量可以提高程序的并發(fā)性,特別是在存在大量阻塞操作(如I/O操作)的情況下。更多的P可以更好地利用CPU資源,減少阻塞時(shí)間。
- P的數(shù)量小于CPU核心數(shù)的影響
- CPU利用率降低:當(dāng)P的數(shù)量小于CPU核心數(shù)時(shí),可能會(huì)導(dǎo)致CPU資源未被充分利用。因?yàn)镻的數(shù)量限制了同時(shí)運(yùn)行的Goroutine數(shù)量,可能會(huì)導(dǎo)致某些CPU核心處于空閑狀態(tài)。
- 減少上下文切換:較少的P數(shù)量可以減少上下文切換的開(kāi)銷,因?yàn)椴僮飨到y(tǒng)需要調(diào)度的線程(M)數(shù)量減少。這可能會(huì)提高CPU密集型任務(wù)的性能。
選擇合適的P數(shù)量選擇合適的P數(shù)量需要根據(jù)具體的應(yīng)用場(chǎng)景和性能需求進(jìn)行調(diào)整。以下是一些建議:
- CPU密集型任務(wù):對(duì)于CPU密集型任務(wù),通常將P的數(shù)量設(shè)置為等于或接近機(jī)器的邏輯CPU核心數(shù),以充分利用CPU資源。
- I/O密集型任務(wù):對(duì)于I/O密集型任務(wù),可以考慮將P的數(shù)量設(shè)置為大于CPU核心數(shù),以提高并發(fā)性和資源利用率。
- 性能測(cè)試和調(diào)優(yōu):通過(guò)性能測(cè)試和調(diào)優(yōu),找到最佳的P數(shù)量設(shè)置??梢試L試不同的P數(shù)量,觀察程序的性能表現(xiàn),選擇最優(yōu)的配置。
Goroutine的上下文切換
Goroutine的上下文切換由Go運(yùn)行時(shí)的調(diào)度器管理,主要涉及以下步驟:
- 保存當(dāng)前Goroutine的狀態(tài):當(dāng)一個(gè)Goroutine被掛起時(shí),Go運(yùn)行時(shí)會(huì)保存當(dāng)前Goroutine的狀態(tài)信息,包括程序計(jì)數(shù)器、棧指針、寄存器等。
- 切換到新的Goroutine:Go運(yùn)行時(shí)會(huì)從P的本地運(yùn)行隊(duì)列中取出下一個(gè)待執(zhí)行的Goroutine,并恢復(fù)其狀態(tài)信息。
- 恢復(fù)新的Goroutine的狀態(tài):Go運(yùn)行時(shí)會(huì)將新的Goroutine的狀態(tài)信息加載到CPU寄存器中,并跳轉(zhuǎn)到新的Goroutine的程序計(jì)數(shù)器位置,繼續(xù)執(zhí)行。
Goroutine什么時(shí)候會(huì)被掛起?Goroutine會(huì)在執(zhí)行阻塞操作、使用同步原語(yǔ)、被調(diào)度器調(diào)度、創(chuàng)建和銷毀時(shí)被掛起。Go運(yùn)行時(shí)通過(guò)高效的調(diào)度機(jī)制管理Goroutine的掛起和恢復(fù),以實(shí)現(xiàn)高并發(fā)和高性能的程序執(zhí)行。了解這些掛起的情況有助于編寫(xiě)高效的并發(fā)程序,并避免潛在的性能問(wèn)題。
- 阻塞操作
當(dāng)Goroutine執(zhí)行阻塞操作時(shí),它會(huì)被掛起,直到阻塞操作完成。常見(jiàn)的阻塞操作包括:
- I/O操作:如文件讀寫(xiě)、網(wǎng)絡(luò)通信等。
- 系統(tǒng)調(diào)用:如調(diào)用操作系統(tǒng)提供的阻塞函數(shù)。
- Channel操作:如在無(wú)緩沖Channel上進(jìn)行發(fā)送或接收操作時(shí),如果沒(méi)有對(duì)應(yīng)的接收者或發(fā)送者,Goroutine會(huì)被掛起。
- 同步原語(yǔ)
使用同步原語(yǔ)(如sync.Mutex、sync.WaitGroup、sync.Cond等)進(jìn)行同步操作時(shí),Goroutine可能會(huì)被掛起,直到條件滿足。例如:
- 互斥鎖(Mutex):當(dāng)Goroutine嘗試獲取一個(gè)已經(jīng)被其他Goroutine持有的互斥鎖時(shí),它會(huì)被掛起,直到鎖被釋放。
- 條件變量(Cond):當(dāng)Goroutine等待條件變量時(shí),它會(huì)被掛起,直到條件變量被通知。
- 調(diào)度器調(diào)度
Go運(yùn)行時(shí)的調(diào)度器會(huì)根據(jù)需要掛起和恢復(fù)Goroutine,以實(shí)現(xiàn)高效的并發(fā)調(diào)度。調(diào)度器可能會(huì)在以下情況下掛起Goroutine:
- 時(shí)間片用完:Go調(diào)度器使用協(xié)作式調(diào)度,當(dāng)一個(gè)Goroutine的時(shí)間片用完時(shí),調(diào)度器會(huì)掛起該Goroutine,并調(diào)度其他Goroutine執(zhí)行。
- 主動(dòng)讓出:Goroutine可以通過(guò)調(diào)用runtime.Gosched()主動(dòng)讓出CPU,調(diào)度器會(huì)掛起該Goroutine,并調(diào)度其他Goroutine執(zhí)行。
- Goroutine的創(chuàng)建和銷毀
- 創(chuàng)建:當(dāng)一個(gè)新的Goroutine被創(chuàng)建時(shí),它會(huì)被掛起,直到調(diào)度器將其調(diào)度執(zhí)行。
- 銷毀:當(dāng)一個(gè)Goroutine執(zhí)行完畢或被顯式終止時(shí),它會(huì)被掛起并從調(diào)度器中移除。
Goroutine的棧管理
Goroutine的??臻g是動(dòng)態(tài)分配的,可以根據(jù)需要自動(dòng)擴(kuò)展。Go運(yùn)行時(shí)使用分段棧(segmented stack)或連續(xù)棧(continuous stack)來(lái)管理Goroutine的??臻g:
- 分段棧:在早期版本的Go中,Goroutine使用分段棧。每個(gè)Goroutine的棧由多個(gè)小段組成,當(dāng)??臻g不足時(shí),Go運(yùn)行時(shí)會(huì)分配新的棧段并鏈接到現(xiàn)有的棧段上。
- 連續(xù)棧:在Go 1.3及以后的版本中,Goroutine使用連續(xù)棧。每個(gè)Goroutine的棧是一個(gè)連續(xù)的內(nèi)存塊,當(dāng)??臻g不足時(shí),Go運(yùn)行時(shí)會(huì)分配一個(gè)更大的棧,并將現(xiàn)有的棧內(nèi)容復(fù)制到新的棧中。
