1. 程式人生 > 實用技巧 >goroutine切換背後那些事兒

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地址。

如下圖:

gg0g0到g的切換是最快的階段。它們包含少量固定的指令,這一點與排程器檢查許多源以尋找下一個要執行的goroutine的情況相反。根據執行程式的情況,這個階段甚至可能需要更多的時間。

需要說明的一點是,對於以上測試的結果會因機器架構的不同而不同


歡迎關注我的微信公眾號: