Goroutine 的切換過程涉及了什么

本文基于 Go 1.13 版本。
Goroutine 很輕,它只需要 2Kb 的內(nèi)存堆棧即可運行。另外,它們運行起來也很廉價,將一個 Goroutine 切換到另一個的過程不牽涉到很多的操作。在深入 Goroutine 切換過程之前,讓我們回顧一下 Goroutine 的切換在更高的層次上是如何進行的。
在繼續(xù)閱讀本文之前,我強烈建議您閱讀我的文章 Go:Goroutine、操作系統(tǒng)線程和 CPU 管理[1] 以了解本文中涉及的一些概念。
案例
Go 根據(jù)兩種斷點將 Goroutine 調(diào)度到線程上:
當 Goroutine 因為系統(tǒng)調(diào)用、互斥鎖或通道而被阻塞時,goroutine 將進入睡眠模式(等待隊列),并允許 Go 調(diào)度運行另一個處于就緒狀態(tài)的 goroutine; 在函數(shù)調(diào)用時,如果 Goroutine 必須增加其堆棧,這會使 Go 調(diào)度另一個 Goroutine 以避免運行中的 Goroutine 獨占 CPU 時間片;
在這兩種情況下,運行調(diào)度程序的 g0 會替換當前的 goroutine,然后選出下一個將要運行的 Goroutine 替換 g0 并在線程上運行。
有關(guān) g0 的更多信息,建議您閱讀我的文章 Go:特殊的 Goroutine g0[2]。
將一個運行中的 Goroutine 切換到另一個的過程涉及到兩個切換:
將運行中的
g切換到g0:
將
g0切換到下一個將要運行的g:
在 Go 中,goroutine 的切換相當輕便,其中需要保存的狀態(tài)僅僅涉及以下兩個:
Goroutine 在停止運行前執(zhí)行的指令,程序當前要運行的指令是記錄在程序計數(shù)器(
PC)中的, Goroutine 稍后將在同一指令處恢復(fù)運行;Goroutine 的堆棧,以便在再次運行時還原局部變量;
讓我們看看實際情況下的切換是怎樣進行的。
程序計數(shù)器
這里通過基于通道的 生產(chǎn)者/消費者模式 來舉例說明,其中一個 Goroutine 產(chǎn)生數(shù)據(jù),而另一些則消費數(shù)據(jù),代碼如下:

消費者僅僅是打印從 0 到 99 的偶數(shù)。我們將注意力放在第一個 goroutine(生產(chǎn)者)上,它將數(shù)字添加到緩沖區(qū)。當緩沖區(qū)已滿時,它將在發(fā)送消息時被阻塞。此時,Go 必須切換到 g0 并調(diào)度另一個 Goroutine 來運行。
如前所述,Go 首先需要保存當前執(zhí)行的指令,以便稍后在同一條指令上恢復(fù) goroutine。程序計數(shù)器(PC)保存在 Goroutine 的內(nèi)部結(jié)構(gòu)中:

可以通過 go tool objdump 命令找到對應(yīng)的指令及其地址,這是生產(chǎn)者的指令:

程序逐條指令的執(zhí)行直到在函數(shù) runtime.chansend1 處阻塞在通道上。Go 將當前程序計數(shù)器保存到當前 Goroutine 的內(nèi)部屬性中。在我們的示例中,Go 使用運行時的內(nèi)部地址 0x4268d0 和方法 runtime.chansend1 保存程序計數(shù)器:

然后,當 g0 喚醒 Goroutine 時,它將在同一指令處繼續(xù)執(zhí)行,繼續(xù)將數(shù)值循環(huán)的推入通道?,F(xiàn)在,讓我們將視線移到 Goroutine 切換期間堆棧的管理。
堆棧
在被阻塞之前,正在運行的 Goroutine 具有其原始堆棧,該堆棧包含臨時存儲器,例如變量 i:

然后,當它在通道上阻塞時,goroutine 將切換到 g0 及其堆棧(更大的堆棧):

在切換之前,堆棧將被保存,以便在 Goroutine 再次運行時進行恢復(fù):

現(xiàn)在,我們對 Goroutine 切換中涉及的不同操作有了一個完整的了解,讓我們繼續(xù)看看它是如何影響性能的。
我們應(yīng)該注意,諸如 arm 等 CPU 架構(gòu)需要再保存一個寄存器,即 LR 鏈接寄存器。
性能
我們?nèi)匀皇褂蒙鲜龅某绦騺頊y量一次切換所需的時間。但是,由于切換時間取決于尋找下一個要調(diào)度的 Goroutine 所花費的時間,因此無法提供完美的性能視圖。在函數(shù)調(diào)用情況下進行的切換要比阻塞在通道上的切換執(zhí)行更多的操作,這也會影響到性能。
讓我們總結(jié)一下我們將要測量的操作:
當前 g阻塞在通道上并切換到g0:PC和堆棧指針一起保存在內(nèi)部結(jié)構(gòu)中將 g0設(shè)置為正在運行的 goroutineg0的堆棧替換當前堆棧g0尋找新的 Goroutine 來運行;g0使用所選的 Goroutine 進行切換:PC和堆棧指針是從其內(nèi)部結(jié)構(gòu)中獲取的程序跳轉(zhuǎn)到對應(yīng)的 PC地址
結(jié)果如下:

從 g 到 g0 或從 g0 到 g 的切換是相當迅速的,它們只包含少量固定的指令。相反,對于調(diào)度階段,調(diào)度程序需要檢查許多資源以便確定下一個要運行的 goroutine,根據(jù)程序的不同,此階段可能會花費更多的時間。
該基準測試給出了性能的數(shù)量級估計,由于沒有標準的工具可以衡量它,所以我們并不能完全依賴于這個結(jié)果。此外,性能也取決于 CPU 架構(gòu)、機器(本文使用的機器是 Mac 2.9 GHz 雙核 Intel Core i5)以及正在運行的程序。
via: https://medium.com/a-journey-with-go/go-what-does-a-goroutine-switch-actually-involve-394c202dddb7
作者:Vincent Blanchon[3]譯者:anxk[4]校對:polaris1119[5]
本文由 GCTT[6] 原創(chuàng)編譯,Go 中文網(wǎng)[7] 榮譽推出
參考資料
Go:Goroutine、操作系統(tǒng)線程和 CPU 管理: https://medium.com/a-journey-with-go/go-goroutine-os-thread-and-cpu-management-2f5a5eaf518a
[2]Go:特殊的 Goroutine g0: https://medium.com/a-journey-with-go/go-g0-special-goroutine-8c778c6704d8
[3]Vincent Blanchon: https://medium.com/@blanchon.vincent
[4]anxk: https://github.com/anxk
[5]polaris1119: https://github.com/polaris1119
[6]GCTT: https://github.com/studygolang/GCTT
[7]Go 中文網(wǎng): https://studygolang.com/
推薦閱讀
