『每周譯Go』Go 的搶占式調(diào)度(文末有彩蛋)
原文地址:https://dtyler.io/articles/2021/03/29/goroutine_preemption_en/
原文作者:Hidetatsu
本文永久鏈接:https://github.com/gocn/translator/blob/master/2021/w19_Preemption_in_Go.md
譯者:lsj1342
校對:guzzsek、fivezh
我正在研究 Go 中 goroutine 的搶占。如果您能指出文中任何錯誤并告知我,將感激不盡。
Go1.14 版本中的搶占行為已經(jīng)發(fā)生了變化。在 Go1.14 中,goroutine 是“異步搶占”的,如發(fā)行版本所述。這意味著什么呢?
首先,讓我們看一個簡單的例子。思考下面的 Go 程序。
package main
import (
"fmt"
)
func main() {
go fmt.Println("hi")
for {
}
}
在主函數(shù)中,啟動了一個只輸出 “hi” 的 goroutine。此外,存在一個無限循環(huán) for {}。
如果我們攜帶參數(shù) GOMAXPROCS=1 運行程序時,將發(fā)生什么呢?程序似乎在輸出 “hi” 后,由于無限循環(huán)而沒有任何反應(yīng)。實際上,我使用 Go1.14 或更高版本運行該程序時(我使用 Go1.16 上運行了該程序(在 WSL2 上的 Ubuntu )),它能按照預(yù)期工作。
有兩種方法可以阻止此程序運行。一種是使用 1.14 之前的 Go 版本運行它。另一種是運行它時攜帶參數(shù) GODEBUG=asyncpreemptoff=1。
當(dāng)我在本地計算機上嘗試時,它的工作方式如下。
$ GOMAXPROCS=1 GODEBUG=asyncpreemptoff=1 go run main.go
# it blocks here
程序沒有輸出 “hi” 。在描述為什么會發(fā)生這種情況之前,讓我先說明幾種使該程序按預(yù)期方式運行的方法。
一種方法是在循環(huán)中添加以下代碼。
*************** package main
*** 2,11 ****
--- 2,13 ----
import (
"fmt"
+ "runtime"
)
func main() {
go fmt.Println("hi")
for {
+ runtime.Gosched()
}
}
runtime.Gosched() 類似于 POSIX 的 sched_yield。sched_yield 強制當(dāng)前線程放棄 CPU,以便其他線程可以運行。之所以命名為 Gosched,因為 Go 中是 goroutine,而不是線程(這是一個猜測)。換句話說,顯式調(diào)用 runtime.Gosched() 將強制對 goroutines 進行重新安排,并且我們期望將當(dāng)前運行的 goroutine 切換到另一個。
另一種方法是使用 GOEXPERIMENT=preemptibleloops。它強制 Go 運行時在“循環(huán)”上進行搶占。這種方式不需要更改代碼。
協(xié)作式調(diào)度 vs 搶占式調(diào)度
首先,有兩種主要的多任務(wù)調(diào)度方法:“協(xié)作”和“搶占”。協(xié)作式多任務(wù)處理也稱為“非搶占”。在協(xié)作式多任務(wù)處理中,程序的切換方式取決于程序本身?!皡f(xié)作”一詞是指這樣一個事實:程序應(yīng)設(shè)計為可互操作的,并且它們必須彼此“協(xié)作”。在搶占式多任務(wù)處理中,程序的切換交給操作系統(tǒng)。調(diào)度是基于某種算法的,例如基于優(yōu)先級,F(xiàn)CSV,輪詢等。
那么現(xiàn)在,goroutine 的調(diào)度是協(xié)作式還是搶占式的?至少在 Go1.13 之前,它是協(xié)作式的。
我沒有找到任何官方文檔,但是我發(fā)現(xiàn)在以下情況會進行 goroutine 切換(并不詳盡)。
等待讀取或?qū)懭胛淳彌_的通道 由于系統(tǒng)調(diào)用而等待 由于 time.Sleep() 而等待 等待互斥量釋放 此外,Go 會啟動一個線程,一直運行著“sysmon”函數(shù),該函數(shù)實現(xiàn)了搶占式調(diào)度(以及其他諸如使網(wǎng)絡(luò)處理的等待狀態(tài)變?yōu)榉亲枞麪顟B(tài))的功能。sysmon 運行在 M(Machine,實際上是一個系統(tǒng)線程),且不需要 P(Processor)。術(shù)語 M,P 和 G 在類似這樣的各種文章中都有解釋。我建議您在需要時參考此類文章。
當(dāng) sysmon 發(fā)現(xiàn) M 已運行同一個 G(Goroutine)10ms 以上時,它會將該 G 的內(nèi)部參數(shù) preempt 設(shè)置為 true。然后,在函數(shù)序言中,當(dāng) G 進行函數(shù)調(diào)用時,G 會檢查自己的 preempt 標(biāo)志,如果它為 true,則它將自己與 M 分離并推入“全局隊列”?,F(xiàn)在,搶占就成功完成。順便說一下,全局隊列是與“本地隊列”不同的隊列,本地隊列是存儲 P 具有的 G。全局隊列有以下幾個作用。
存儲那些超過本地隊列容量(256)的 G
存儲由于各種原因而等待的 G
存儲由搶占標(biāo)志分離的 G
這是 Go1.13 及其之前版本的實現(xiàn)?,F(xiàn)在,您將了解為什么上面的無限循環(huán)代碼無法按預(yù)期工作。for{} 僅僅是一個死循環(huán),所以如前所述它不會觸發(fā) goroutine 切換。您可能會想,“sysmon 是否設(shè)置了搶占標(biāo)志,因為它已經(jīng)運行了 10ms 以上?” 然而,如果沒有函數(shù)調(diào)用,即使設(shè)置了搶占標(biāo)志,也不會進行該標(biāo)志的檢查。如前所述,搶占標(biāo)志的檢查發(fā)生在函數(shù)序言中,因此不執(zhí)行任何操作的死循環(huán)不會發(fā)生搶占。
是的,隨著 Go1.14 中引入“非協(xié)作式搶占”(異步搶占),這種行為已經(jīng)改變。
“異步搶占”是什么意思?
讓我們總結(jié)到目前為止的要點;Go 具有一種稱為“sysmon”的機制,可以監(jiān)視運行 10ms 以上的 goroutine 并在必要時強制搶占。但是,由于它的工作方式,在 for{} 的情況下并不會發(fā)生搶占。
Go1.14 引入非協(xié)作式搶占,即搶占式調(diào)度,是一種使用信號的簡單有效的算法。
首先,sysmon 仍然會檢測到運行了 10ms 以上的 G(goroutine)。然后,sysmon 向運行 G 的 P 發(fā)送信號(SIGURG)。Go 的信號處理程序會調(diào)用P上的一個叫作 gsignal 的 goroutine 來處理該信號,將其映射到 M 而不是 G,并使其檢查該信號。gsignal 看到搶占信號,停止正在運行的 G。
由于此機制會顯式發(fā)出信號,因此無需調(diào)用函數(shù),就能將正在運行死循環(huán)的 goroutine 切換到另一個 goroutine。
通過使用信號的異步搶占機制,上面的代碼現(xiàn)在就可以按預(yù)期工作。GODEBUG=asyncpreemptoff=1可用于禁用異步搶占。
順便說一句,他們選擇使用 SIGURG,是因為 SIGURG 不會干擾現(xiàn)有調(diào)試器和其他信號的使用,并且因為它不在 libc 中使用。(參考)
總結(jié)
不執(zhí)行任何操作的無限循環(huán)不會將 CPU 傳遞給其他 goroutine,并不意味著 Go1.13 之前的機制是不好的。正如 @davecheney 所說,通常不認(rèn)為這是一個特殊問題。起初,異步搶占不是為了解決無限循環(huán)問題引出的。
盡管異步搶占的引入使調(diào)度更具搶占性,但也有必要在 GC 期間更加謹(jǐn)慎地處理“不安全點”。在這方面對實現(xiàn)上的考慮也非常有趣。有興趣的讀者可以自己閱讀議題:非協(xié)作式 goroutine 搶占。
參考
Proposal: Non-cooperative goroutine preemption runtime: non-cooperative goroutine preemption runtime: tight loops should be preemptible runtime: golang scheduler is not preemptive - it’s cooperative? Source file src/runtime/preempt.go Goroutine preemptive scheduling with new features of go 1.14 Go: Goroutine and Preemption At which point a goroutine can yield? Go: Asynchronous Preemption go routine blocking the others one [duplicate] (Ja) Golangのスケジューラあたりの話 (Ja) goroutineがスイッチされるタイミング
NEWS
在這次 GopherChina 2021大會上,曹春暉老師將與 Gopher 們分享 “Go 的搶占式調(diào)度”相關(guān)的精彩內(nèi)容。

