當一個 goroutine 創建新的 goroutine 時,scheduler 會選誰優先執行?

Genchi Lu
4 min readJan 30, 2019

--

Golang 的 concurrency 運作機制大致如下:

由 context (P) 從 queue 中挑選 goroutine (G) 給 OS thread (M) 執行。當某個 thread 的 queue 為空時,context(P) 會去其他 thread 的 queue 偷 goroutine 還執行。

那麼,當一個 goroutine 創建一個 goroutine 時,到底 P 會選擇那一個繼續執行呢?

Concurrency in Go 一書是這樣說的:

when creating a goroutine, it is very likely that your program will want the function in that goroutine to execute. It is also reasonably likely that the continuation from that goroutine will at some point want to join with that goroutine. And it’s not uncommon for the continuation to attempt a join before the goroutine has finished completing. Given these axioms, when scheduling a goroutine, it makes sense to immediately begin working on it.

當創建一個新的 goroutine 時,通常會希望該 goroutine 執行完程式碼再 join 回原本的 goroutine;因此比較合理的做法是 scheduler 先挑選新創建的 goroutine 給 thread 執行,將原本的 goroutine 丟到 queue 裡面等待。

既然 scheduler 總是挑選新創建的 goroutine 執行,若是 goroutine 創建出來以後必須等待原先的 goroutine 產出,那麼 scheduler 就會額外浪費一小段無意義的時間在切換 goroutine (執行新創建的 goroutine 後發現需要等待原本的 goroutine,於是切回原本的 goroutine 執行,再切回新創建的 goroutine)。

我覺得這概念很有趣,為了驗證,我寫了一小段程式碼模擬這情境:

其中

  1. consume function 接收一個 input channel,並創建一個 goroutine 去 select input channel。
  2. produce function 接收一個 output channel,隨意產生一些數學運算並將結果傳入 output channel。
  3. BenchmarkProducerFirst 會先將 buffer 丟給 produce function 執行100000 次,再將 buffer channel 傳給 consume 消化。
  4. BenchmarkComsumerFirst 則相反,buffer channel 先丟給 consume 消化 1000 次,再呼叫 produce 去填充 buffer。

以下是我在 MacBook Pro 2017 上面跑 benchmark 的結果:

BenchmarkProducerFirst-4        2000000000               0.06 ns/op
BenchmarkComsumerFirst-4 2000000000 0.15 ns/op

僅僅只是更改 produce/consume 的順序,效能差距比我預期的還大!

go tool trace觀察 Scheduler latency profile:

Scheduler latency profile of BenchmarkProducerFirst
Scheduler latency profile of BenchmarkComsumerFirst

可以看到先 produce 的 benchmark 在 Scheduler latency 明顯優於先 consume (9834345.77us vs 306062.93ms)。

當然實務我認為這點效能差距並不會造成多大的問題,除非是你的系統對效能有極其極端的要求。這篇文章僅僅只是一個嘗試驗證 go scheduler 採用的演算法的概念。

--

--