goroutine切換背後那些事兒
本文基於於GoLang 1.13。
1. 寫在前面
微信公眾號:[double12gzh]
個人主頁: https://gzh.readthedocs.io
關注容器技術、關注
Kubernetes
。問題或建議,請公眾號留言。
Goroutine很輕量,從資源消耗方面來看,它只需要一個2Kb的記憶體棧就可以執行;從執行時來看,它的執行成本也很低,將一個goroutine切換到另一個goroutine並不需要很多操作。
在進行講解golang的切換之前,我們先High Level的看一下goroutine切換的相關內容。
2. 案例
golang會根據兩種斷點將goroutine排程到執行緒上:
-
當一個goroutine阻塞了。如:系統呼叫,mutex,或者通道。被阻塞的goroutine會進入睡眠模式/佇列,讓Go排程並執行一個等待的goroutine。被阻塞的goroutine進入睡眠模式/佇列,允許Go排程和執行一個等待的goroutine。
-
在函式呼叫過程中,假如goroutine必須增長它的棧。這個斷點允許Go排程另一個goroutine,避免正在執行的那個goroutine佔用CPU。
在這兩種情況下,執行排程器的g0會用另一個準備執行的goroutine替換當前的goroutine。然後,被選中的goroutine取代g0,從而線上程上執行。
如果您想了解更多關於
g0
的內容,請參考g0
將一個執行中的goroutine切換到另一個執行中的goroutine涉及到兩個切換。
g
->g0
。
g0
-> 另一個g
。
在GoLang中,groutine真的非常輕量。為了儲存,它只需要兩個東西:
-
goroutine是在哪一行停止的。即:在被排程前,goroutine是在哪一行停止的,當前要執行的指令被記錄在程式計數器(PC)中。goroutine稍後將在同一點恢復。
-
存放goroutine的堆疊。這個堆疊的目的是為了方便再次執行時恢復其區域性變數。
下面我們深入看一下。
3. PC(程式記數器)
為了便於舉例,我將使用一個通過channel進行通訊的goroutine來說明,這兩個goroutine中,一個可以產生資料的,其它的用於消費資料。程式碼如下:
package main
import (
"fmt"
"sync"
)
const COUNT = 100
func main() {
var wg sync.WaitGroup
c := make(chan int, 10)
wg.Add(1)
// 生產資料
go func() {
for i := 0; i < COUNT; i++ {
c <- i
}
close(c)
wg.Done()
}()
// 消費資料
for i := 0; i < 3; i++ {
wg.Add(1)
go func() {
for v := range c {
if v%2 == 0 {
fmt.Println(v)
}
}
}()
}
wg.Wait()
}
消費者基本上會列印0到99的偶數,我們將重點關注第一個goroutine--生產者--向緩衝區新增數字。當緩衝區滿了,它將在傳送訊息時阻塞。此時,Go要切換到g0,排程另一個goroutine。
如前所述,Go首先需要儲存當前指令,以便在同一指令處恢復goroutine。程式計數器(PC)儲存在goroutine的內部結構中。
上面的程式碼可以使用以下圖來簡單說明:
指令和它們的地址可以通過命令獲取:
➜ hello go tool compile -N -l main.go
➜ hello ls | grep main.o
main.o
下面是生產者的一個示例:
➜ hello go tool objdump main.o
在函式runtime.chansend1
上阻塞通道前,程式逐條指令執行。Go將當前的程式計數器儲存到當前goroutine的內部屬性中。在我們的例子中,Go儲存程式計數器的地址是0x4268d0
,這個地址是在runtime
和方法runtime.chansend1
內部的。
然後,當g0
喚醒goroutine時,它將在同一指令處恢復,對數值進行迴圈並推入通道。
下面我們來談談goroutine切換過程中的棧管理。
4. 棧(stack)
在被阻塞之前,正在執行的goroutine有它的原始棧。這個堆疊包含臨時記憶體,比如變數i:
然後,當它在通道上阻塞時,goroutine將和它的堆疊一起切換到g0,這個goroutine將會有一個更大的棧。
在切換之前,堆疊將被儲存,以便在goroutine再次執行時恢復。
我們現在已經完整地瞭解了goroutine切換中涉及的不同操作。現在讓我們看看它是如何影響效能的。
我們應該注意到,一些架構(比如arm)需要多儲存一個暫存器LR(連結暫存器)。
5. 操作
為了測量goroutine切換可能需要的時間,我們將使用前面寫的程式。然而,它並不能給出一個完美的效能檢視,因為它可能取決於找到下一個要排程的goroutine所需的時間。這樣goroutine的切換也會影響效能,從函式prolog的切換比從通道上阻塞的goroutine切換要做的操作更多。
我們來總結一下我們要測量的操作:
- 當前的g在通道上阻塞並切換到g0:
- PC和堆疊指標一起被儲存在一個內部結構中
- g0被設定為正在執行的goroutine。
- g0的堆疊取代了當前的堆疊。
- g0正在尋找一個新的goroutine來執行。
- g0必須與所選的goroutine進行切換。
- PC和堆疊指標被從內部結構中提取出來。
- 程式跳轉到獲取的PC地址。
如下圖:
從g
到g0
或g0到
g的切換是最快的階段。它們包含少量固定的指令,這一點與排程器檢查許多源以尋找下一個要執行的goroutine的情況相反。根據執行程式的情況,這個階段甚至可能需要更多的時間。
需要說明的一點是,對於以上測試的結果會因機器架構的不同而不同
歡迎關注我的微信公眾號: