Go_Goroutine詳解
Goroutine詳解
goroutine的概念類似於執行緒,但 goroutine是由Go的運行時(runtime)調度和管理的。Go程式會智慧地將 goroutine 中的任務合理地分配給每個CPU。Go語言之所以被稱為現代化的程式語言,就是因為它在語言層面已經內置了調度和上下文切換的機制。
在Go語言編程中你不需要去自己寫進程、執行緒、協程,你的技能包里只有一個技能–goroutine,當你需要讓某個任務並發執行的時候,你只需要把這個任務包裝成一個函數,開啟一個goroutine去執行這個函數就可以了,就是這麼簡單粗暴。
使用goroutine
Go語言中使用goroutine非常簡單,只需要在調用函數的時候在前面加上go關鍵字,就可以為一個函數創建一個goroutine。
一個goroutine必定對應一個函數,可以創建多個goroutine去執行相同的函數。
啟動單個goroutine
啟動goroutine的方式非常簡單,只需要在調用的函數(普通函數和匿名函數)前面加上一個go關鍵字。
舉個例子如下:
func hello() {
fmt.Println("Hello Goroutine!")
}
func main() {
hello()
fmt.Println("main goroutine done!")
}
這個示例中hello函數和下面的語句是串列的,執行的結果是列印完Hello Goroutine!後列印main goroutine done!。
接下來我們在調用hello函數前面加上關鍵字go,也就是啟動一個goroutine去執行hello這個函數。
func main() {
go hello() // 啟動另外一個goroutine去執行hello函數
fmt.Println("main goroutine done!")
}
這一次的執行結果只列印了main goroutine done!,並沒有列印Hello Goroutine!。為什麼呢?
在程式啟動時,Go程式就會為main()函數創建一個默認的goroutine。
當main()函數返回的時候該goroutine就結束了,所有在main()函數中啟動的goroutine會一同結束,main函數所在的goroutine就像是權利的遊戲中的夜王,其他的goroutine都是異鬼,夜王一死它轉化的那些異鬼也就全部GG了。
所以我們要想辦法讓main函數等一等hello函數,最簡單粗暴的方式就是time.Sleep了。
func main() {
go hello() // 啟動另外一個goroutine去執行hello函數
fmt.Println("main goroutine done!")
time.Sleep(time.Second)
}
執行上面的程式碼你會發現,這一次先列印main goroutine done!,然後緊接著列印Hello Goroutine!。
首先為什麼會先列印main goroutine done!是因為我們在創建新的goroutine的時候需要花費一些時間,而此時main函數所在的goroutine是繼續執行的。
啟動多個goroutine
在Go語言中實現並發就是這樣簡單,我們還可以啟動多個goroutine。讓我們再來一個例子: (這裡使用了sync.WaitGroup來實現goroutine的同步)
var wg sync.WaitGroup
func hello(i int) {
defer wg.Done() // goroutine結束就登記-1
fmt.Println("Hello Goroutine!", i)
}
func main() {
for i := 0; i < 10; i++ {
wg.Add(1) // 啟動一個goroutine就登記+1
go hello(i)
}
wg.Wait() // 等待所有登記的goroutine都結束
}
多次執行上面的程式碼,會發現每次列印的數字的順序都不一致。這是因為10個goroutine是並發執行的,而goroutine的調度是隨機的。
goroutine與執行緒
可增長的棧
OS執行緒(作業系統執行緒)一般都有固定的棧記憶體(通常為2MB),一個goroutine的棧在其生命周期開始時只有很小的棧(典型情況下2KB),goroutine的棧不是固定的,他可以按需增大和縮小,goroutine的棧大小限制可以達到1GB,雖然極少會用到這個大。所以在Go語言中一次創建十萬左右的goroutine也是可以的。
goroutine調度
GPM是Go語言運行時(runtime)層面的實現,是go語言自己實現的一套調度系統。區別於作業系統調度OS執行緒。
- 1.G很好理解,就是個goroutine的,裡面除了存放本goroutine資訊外 還有與所在P的綁定等資訊。
- 2.P管理著一組goroutine隊列,P裡面會存儲當前goroutine運行的上下文環境(函數指針,堆棧地址及地址邊界),P會對自己管理的goroutine隊列做一些調度(比如把佔用CPU時間較長的goroutine暫停、運行後續的goroutine等等)當自己的隊列消費完了就去全局隊列里取,如果全局隊列里也消費完了會去其他P的隊列里搶任務。
- 3.M(machine)是Go運行時(runtime)對作業系統內核執行緒的虛擬, M與內核執行緒一般是一一映射的關係, 一個groutine最終是要放到M上執行的;
P與M一般也是一一對應的。他們關係是: P管理著一組G掛載在M上運行。當一個G長久阻塞在一個M上時,runtime會新建一個M,阻塞G所在的P會把其他的G 掛載在新建的M上。當舊的G阻塞完成或者認為其已經死掉時 回收舊的M。
P的個數是通過runtime.GOMAXPROCS設定(最大256),Go1.5版本之後默認為物理執行緒數。 在並發量大的時候會增加一些P和M,但不會太多,切換太頻繁的話得不償失。
單從執行緒調度講,Go語言相比起其他語言的優勢在於OS執行緒是由OS內核來調度的,goroutine則是由Go運行時(runtime)自己的調度器調度的,這個調度器使用一個稱為m:n調度的技術(復用/調度m個goroutine到n個OS執行緒)。 其一大特點是goroutine的調度是在用戶態下完成的, 不涉及內核態與用戶態之間的頻繁切換,包括記憶體的分配與釋放,都是在用戶態維護著一塊大的記憶體池, 不直接調用系統的malloc函數(除非記憶體池需要改變),成本比調度OS執行緒低很多。 另一方面充分利用了多核的硬體資源,近似的把若干goroutine均分在物理執行緒上, 再加上本身goroutine的超輕量,以上種種保證了go調度方面的性能。