Go 的搶佔式排程
Go 的搶佔式排程
- 原文地址: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 版本中的搶佔行為已經發生了變化。在 Go1.14 中,goroutine 是 “非同步搶佔” 的,如發行版本所述。這意味著什麼呢?
首先,讓我們看一個簡單的例子。思考下面的 Go 程式。
package main
import (
"fmt"
)
func main() {
go fmt.Println("hi")
for {
}
}
在主函式中,啟動了一個只輸出 “hi” 的 goroutine。此外,存在一個無限迴圈 for {}
。
如果我們攜帶引數 GOMAXPROCS=1
執行程式時,將發生什麼呢?程式似乎在輸出 “hi” 後,由於無限迴圈而沒有任何反應。實際上,我使用 Go1.14 或更高版本執行該程式時(我使用 Go1.16 上執行了該程式(在 WSL2 上的 Ubuntu )),它能按照預期工作。
有兩種方法可以阻止此程式執行。一種是使用 1.14 之前的 Go 版本執行它。另一種是執行它時攜帶引數 GODEBUG=asyncpreemptoff=1
。
當我在本地計算機上嘗試時,它的工作方式如下。
$ GOMAXPROCS=1 GODEBUG=asyncpreemptoff=1 go run main.go
# it blocks here
程式沒有輸出 “hi” 。在描述為什麼會發生這種情況之前,讓我先說明幾種使該程式按預期方式執行的方法。
一種方法是在迴圈中新增以下程式碼。
*************** 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
強制當前執行緒放棄 CPU,以便其他執行緒可以執行。之所以命名為 Gosched
,因為 Go 中是 goroutine,而不是執行緒(這是一個猜測)。換句話說,顯式呼叫 runtime.Gosched() 將強制對 goroutines 進行重新安排,並且我們期望將當前執行的 goroutine 切換到另一個。
另一種方法是使用 GOEXPERIMENT=preemptibleloops。它強制 Go 執行時在 “迴圈” 上進行搶佔。這種方式不需要更改程式碼。
協作式排程 vs 搶佔式排程
首先,有兩種主要的多工排程方法:“協作” 和 “搶佔”。協作式多工處理也稱為 “非搶佔”。在協作式多工處理中,程式的切換方式取決於程式本身。“協作” 一詞是指這樣一個事實:程式應設計為可互操作的,並且它們必須彼此 “協作”。在搶佔式多工處理中,程式的切換交給作業系統。排程是基於某種演算法的,例如基於優先順序,FCSV,輪詢等。
那麼現在,goroutine 的排程是協作式還是搶佔式的?至少在 Go1.13 之前,它是協作式的。
我沒有找到任何官方文件,但是我發現在以下情況會進行 goroutine 切換(並不詳盡)。
- 等待讀取或寫入未緩衝的通道
- 由於系統呼叫而等待
- 由於 time.Sleep() 而等待
- 等待互斥量釋放
此外,Go 會啟動一個執行緒,一直執行著 “sysmon” 函式,該函式實現了搶佔式排程(以及其他諸如使網路處理的等待狀態變為非阻塞狀態)的功能。sysmon 執行在 M(Machine,實際上是一個系統執行緒),且不需要 P(Processor)。術語 M,P 和 G 在類似這樣的各種文章中都有解釋。我建議您在需要時參考此類文章。
當 sysmon 發現 M 已執行同一個 G(Goroutine)10ms 以上時,它會將該 G 的內部引數 preempt
設定為 true。然後,在函式序言中,當 G 進行函式呼叫時,G 會檢查自己的 preempt
標誌,如果它為 true,則它將自己與 M 分離並推入 “全域性佇列”。現在,搶佔就成功完成。順便說一下,全域性佇列是與 “本地佇列” 不同的佇列,本地佇列是儲存 P 具有的 G。全域性佇列有以下幾個作用。
- 儲存那些超過本地佇列容量(256)的 G
- 儲存由於各種原因而等待的 G
- 儲存由搶佔標誌分離的 G
這是 Go1.13 及其之前版本的實現。現在,您將瞭解為什麼上面的無限迴圈程式碼無法按預期工作。for{}
僅僅是一個死迴圈,所以如前所述它不會觸發 goroutine 切換。您可能會想,“sysmon 是否設定了搶佔標誌,因為它已經執行了 10ms 以上?” 然而,如果沒有函式呼叫,即使設定了搶佔標誌,也不會進行該標誌的檢查。如前所述,搶佔標誌的檢查發生在函式序言中,因此不執行任何操作的死迴圈不會發生搶佔。
是的,隨著 Go1.14 中引入 “非協作式搶佔”(非同步搶佔),這種行為已經改變。
“非同步搶佔” 是什麼意思?
讓我們總結到目前為止的要點;Go 具有一種稱為 “sysmon” 的機制,可以監視執行 10ms 以上的 goroutine 並在必要時強制搶佔。但是,由於它的工作方式,在 for{}
的情況下並不會發生搶佔。
Go1.14 引入非協作式搶佔,即搶佔式排程,是一種使用訊號的簡單有效的演算法。
首先,sysmon 仍然會檢測到執行了 10ms 以上的 G(goroutine)。然後,sysmon 向執行 G 的 P 傳送訊號(SIGURG
)。Go 的訊號處理程式會呼叫 P 上的一個叫作 gsignal
的 goroutine 來處理該訊號,將其對映到 M 而不是 G,並使其檢查該訊號。gsignal 看到搶佔訊號,停止正在執行的 G。
由於此機制會顯式發出訊號,因此無需呼叫函式,就能將正在執行死迴圈的 goroutine 切換到另一個 goroutine。
通過使用訊號的非同步搶佔機制,上面的程式碼現在就可以按預期工作。GODEBUG=asyncpreemptoff=1
可用於禁用非同步搶佔。
順便說一句,他們選擇使用 SIGURG,是因為 SIGURG 不會干擾現有偵錯程式和其他訊號的使用,並且因為它不在 libc 中使用。(參考)
總結
不執行任何操作的無限迴圈不會將 CPU 傳遞給其他 goroutine,並不意味著 Go1.13 之前的機制是不好的。正如 @davecheney 所說,通常不認為這是一個特殊問題。起初,非同步搶佔不是為了解決無限迴圈問題引出的。
儘管非同步搶佔的引入使排程更具搶佔性,但也有必要在 GC 期間更加謹慎地處理 “不安全點”。在這方面對實現上的考慮也非常有趣。有興趣的讀者可以自己閱讀議題:非協作式 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 がスイッチされるタイミング
- 加微信實戰群請加微信(註明:實戰群):gocnio
相關文章
- linux搶佔式排程Linux
- Go1.12將支援搶佔式goroutine排程Go
- 非可搶佔式和搶佔式程式排程的區別是什麼?
- go1.14 基於訊號的搶佔式排程實現原理Go
- Go runtime 排程器精講(十):非同步搶佔Go非同步
- async-await:協作排程 vs 搶佔排程AI
- Go runtime 排程器精講(九):系統呼叫引起的搶佔Go
- Go runtime 排程器精講(八):執行時間過長的搶佔Go
- kube-scheduler原始碼分析(3)-搶佔排程分析原始碼
- Keepalived+Nginx高可用案例(搶佔式與非搶佔式)Nginx
- Go Runtime 的排程器Go
- Go 排程模型 GPMGo模型
- Go runtime 排程器精講(五):排程策略Go
- Go語言排程器之主動排程(20)Go
- Go排程器系列(3)圖解排程原理Go圖解
- Go語言排程器之排程main goroutine(14)GoAI
- Go排程器系列(2)巨集觀看排程器Go
- Go timer 是如何被排程的?Go
- keepalived 高可用(非搶佔式)
- 帶有分散式鎖的Go計劃任務排程器- DEV分散式Godev
- Go runtime 排程器精講(二):排程器初始化Go
- 【深入淺出 Yarn 架構與實現】5-3 Yarn 排程器資源搶佔模型Yarn架構模型
- 分散式任務排程分散式
- Go 併發程式設計 - runtime 協程排程(三)Go程式設計
- 分散式排程任務-ElasticJob分散式AST
- Flink排程之排程器、排程策略、排程模式模式
- Go runtime 排程器精講(一):Go 程式初始化Go
- 【深入理解Go】協程設計與排程原理(下)Go
- 【深入理解Go】協程設計與排程原理(上)Go
- Go runtime 排程器精講(七):案例分析Go
- Go語言goroutine排程器初始化Go
- go 原始碼分析 goroutine 概覽與排程Go原始碼
- LTS分散式任務排程部署分散式
- go語言實戰課程《Go語言開發分散式任務排程 輕鬆搞定高效能Crontab》——推薦分享Go分散式
- Caffeinated 6.828:實驗 4:搶佔式多工處理
- 排程器簡介,以及Linux的排程策略Linux
- Go runtime 排程器精講(三):main goroutine 建立GoAI
- 2. Go併發程式設計--GMP排程Go程式設計