相對於操作系統線程,Goroutine 的開銷十分小,一個 Goroutine 的起始棧大小為 2KB,而且創建、切換與銷毀的代價很低,可以創建成千上萬甚至更多 Goroutine。所以和其他語言不同的是,Go 應用通常可以為每個新建立的連接創建一個對應的新 Goroutine,甚至是為每個傳入的請求生成一個 Goroutine 去處理。
Goroutine 的開銷雖然“廉價”,但也不是免費的
一旦規模化后,這種非零成本也會成為瓶頸。以一個 Goroutine 分配 2KB 執行棧為例,100w Goroutine 就是 2GB 的內存消耗。
其次,Goroutine 從Go 1.4 版本開始采用了連續棧的方案,也就是每個 Goroutine 的執行棧都是一塊連續內存,如果空間不足,運行時會分配一個更大的連續內存空間作為這個 Goroutine 的執行棧,將原棧內容拷貝到新分配的空間中來。連續棧的原理決定了,一旦 Goroutine 的執行棧發生了 grow,那么即便這個 Goroutine 不再需要那么大的棧空間,這個 Goroutine 的棧空間也不會被 Shrink(收縮)了,這些空間可能會處於長時間閑置的狀態,直到 Goroutine 退出。
另外,隨着 Goroutine 數量的增加,Go 運行時進行 Goroutine 調度的處理器消耗,也會隨之增加,成為阻礙 Go 應用性能提升的重要因素。
Goroutine 池是一種常見的解決方案。這個方案的核心思想是對 Goroutine 的重用,也就是把 M 個計算任務調度到 N 個 Goroutine 上,而不是為每個計算任務分配一個獨享的 Goroutine,從而提高計算資源的利用率。
workerpool 的實現原理
workerpool 有很多種實現方式,這里為了更好地演示 Go 並發模型的應用模式,以及並發原語間的協作,這里采用完全基於 channel+select 的實現方案,不使用其他數據結構,也不使用 sync 包提供的各種同步結構,比如 Mutex、RWMutex,以及 Cond 等。
workerpool 的實現主要分為三個部分:
-
pool 的創建與銷毀;
-
pool 中 worker(Goroutine)的管理;
-
task 的提交與調度。
capacity 是 pool 的一個屬性,代表整個 pool 中 worker 的最大容量。使用一個帶緩沖的 channel:active,作為 worker 的“計數器”
workerpool 文件對外主要提供三個 API:
- workerpool.New:用於創建一個 pool 類型實例,並將 pool 池的 worker 管理機制運行起來;
- workerpool.Free:用於銷毀一個 pool 池,停掉所有 pool 池中的 worker;
- Pool.Schedule:這是 Pool 類型的一個導出方法,workerpool 包的用戶通過該方法向 pool 池提交待執行的任務(Task)。