Goroutine

2021-03-02 有痕的技術分析

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

相關焦點

  • GO語言:協程——Goroutine
    1.2 主goroutine 封裝main函數的goroutine稱為主goroutine。 主goroutine所做的事情並不是執行main函數那麼簡單。它首先要做的是:設定每一個goroutine所能申請的棧空間的最大尺寸。
  • 第 77 期周刊題解:關於 goroutine 數量的,你答對了嗎?
    如果洩漏,洩漏了多少個goroutine?怎麼答不進行resp.Body.Close(),洩漏是一定的。但是洩漏的goroutine個數就讓我迷糊了。 go pconn.readLoop()  // 啟動一個讀goroutine go pconn.writeLoop() // 啟動一個寫goroutine return pconn, nil}一次建立連接,就會啟動一個讀goroutine和寫goroutine。這就是為什麼一次http.Get()會洩漏兩個goroutine的來源。
  • 更便捷的goroutine控制利器- Context
    可以將相同的上下文傳遞給在不同goroutine中運行的函數。上下文可以安全地被多個goroutine同時使用巴拉巴拉,說了一大堆,反正我一句沒懂,當然我知道context是幹嘛的,(尬~,不小心暴露了,學渣的本質),說說我的理解以及使用建議對伺服器的傳入請求應創建一個Context,而對伺服器的傳出響應也應接受一個Context。
  • Go 基礎之 Goroutine
    我們將討論關於性能方面的一些知識,並通過創建一些簡單的 goroutine 來擴展我們的應用程式。我們還會關注一些 Go 語言的底層執行邏輯以及 Go 語言與其他語言的不同之處。Go 語言的並發繼續討論之前,我們必須理解並發與並行的概念。Golang 可以實現並發和並行。我們一起來看下並發與並行的區別。
  • Go語言潛力有目共睹,但它的Goroutine機制底層原理你了解嗎?
    Goroutine在整個生存期也存在不同的狀態切換,主要的有以下幾種狀態:畫個狀態圖看下:巨人的肩膀https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-goroutine
  • Go 中的 Goroutine 和其他並發處理方案的對比
    針對這個問題,目前我看到的基於通道的解決方案是將一個回復通道作為查詢消息的一部分一起發送給主 goroutine(通過共享通道發送)。但是這種方法的有個副作用,那就是通道的頻繁分配和釋放,每次請求都會對通道進行分配、初始化、使用一次然後銷毀(我認為這些通道必須通過垃圾回收機制回收,而不是在棧上分配和釋放)。
  • 面試題:Goroutine 初始佔用多大棧空間?是如何演進的
    via: https://medium.com/a-journey-with-go/go-how-does-the-goroutine-stack-size-evolve-447fc02085e5作者:Vincent Blanchon[10]譯者:Jun10ng[11]校對:polaris1119