Goroutine
Go不需要像C++或者Java那樣手動管理線程,Go語言的goroutine機制自動幫你管理線程。
使用goroutine、
Go語言中使用goroutine非常簡單,只需要在調用函數的時候在前面加上go關鍵字,就可以為一個函數創建一個goroutine。
一個goroutine必定對應一個函數,可以創建多個goroutine去執行相同的函數
啟動單個goroutine
在調用的函數前加上一個go關鍵字,就可以啟動一個goroutine
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了。```go
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
來看下面這個代碼:
var wg sync.WaitGroupfunc hello(i int) {defer wg.Done() // goroutine結束就登記-1fmt.Println("Hello Goroutine!", i)
}
func main() {for i := 0; i < 10; i++ {wg.Add(1) // 啟動一個goroutine就登記+1go hello(i)}wg.Wait() // 等待所有登記的goroutine都結束
}
多次執行上面的代碼,會發現每次打印的數字的順序都不一致。這是因為10個goroutine是并發執行的,而goroutine的調度是隨機的
如果主協程退出了,其他任務還執行嗎(運行下面的代碼測試一下吧)
package mainimport ("fmt""time"
)func main() {// 合起來寫go func() {i := 0for {i++fmt.Printf("new goroutine: i = %d\n", i)time.Sleep(time.Second)}}()i := 0for {i++fmt.Printf("main goroutine: i = %d\n", i)time.Sleep(time.Second)if i == 2 {break}}
}
是停止的
goroutine與線程
可增長的棧
OS線程(操作系統線程)一般都有固定的棧內存(通常為2MB),一個goroutine的棧在其生命周期開始時只有很小的棧(典型情況下2KB),goroutine的棧不是固定的,他可以按需增大和縮小,goroutine的棧大小限制可以達到1GB,雖然極少會用到這個大。所以在Go語言中一次創建十萬左右的goroutine也是可以的。
goroutine的調度
GPM是Go語言運行時(runtime)層面的實現,是go語言自己實現的一套調度系統。區別于操作系統調度OS線程。
- G很好理解,就是個goroutine的,里面除了存放本goroutine信息外 還有與所在P的綁定等信息。
- P管理著一組goroutine隊列,P里面會存儲當前goroutine運行的上下文環境(函數指針,堆棧地址及地址邊界),P會對自己管理的goroutine隊列做一些調度(比如把占用CPU時間較長的goroutine暫停、運行后續的goroutine等等)當自己的隊列消費完了就去全局隊列里取,如果全局隊列里也消費完了會去其他P的隊列里搶任務。
- 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調度方面的性能。