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調度方面的性能。

Tags: