啟動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的調度是隨機的。
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調度方面的性能。