Go語言如何實(shí)現(xiàn)stop the world?
本文基于 Go 1.13。
在某些垃圾回收器算法中,“停止世界”(Stop the World: STW,下同)是跟蹤內(nèi)存使用最重要的階段,它會(huì)停止程序的執(zhí)行,以掃描內(nèi)存使用,并添加寫障礙。讓我們回顧一下它在內(nèi)部如何工作,以及它可能面臨的潛在問題。
停止世界(Stop the world)
停止程序意味著停止所有正在運(yùn)行的 goroutine。下面是一個(gè)執(zhí)行 STW 的簡單程序:
- func main() {
- runtime.GC()
- }
運(yùn)行垃圾回收器,將觸發(fā) STW 兩個(gè)階段。
有關(guān)垃圾回收器周期的更多信息,建議閱讀我的另外一篇文章 “Go:垃圾收集器如何標(biāo)記內(nèi)存? ① ”
第一步:搶占所有正在運(yùn)行的 goroutine:
goroutine 搶占
一旦 goroutine 被搶占,它們將在安全點(diǎn)停止。同時(shí),P 處理器將(正在運(yùn)行的代碼或在空閑列表)被標(biāo)記為已停止,以不運(yùn)行任何代碼:
P 標(biāo)記為已停止
然后,Go 調(diào)度程序?qū)⑦\(yùn)行,將每個(gè) M 與其 P 各自分離,并將其放入空閑列表中:
M 已移至閑置清單
關(guān)于在每個(gè)上運(yùn)行的 goroutine M,它們將在全局隊(duì)列中等待:
Goroutine 在全局隊(duì)列中等待
然后,一旦世界停止了,只有唯一活動(dòng)的 goroutine 才能安全地運(yùn)行,并在工作完成后啟動(dòng)整個(gè)世界。下面跟蹤圖將有助于理解此階段發(fā)生在何時(shí):
跟蹤 “ STW”階段
系統(tǒng)調(diào)用
“STW”階段也可能會(huì)影響系統(tǒng)調(diào)用,因?yàn)樗鼈兛赡軙?huì)在 STW 時(shí)返回。讓我們以一個(gè)密集執(zhí)行系統(tǒng)調(diào)用的例子,并查看其如何處理:
- func main() {
- var wg sync.WaitGroup
- wg.Add(10)
- for i := 0; i < 10; i++ {
- go func() {
- http.Get(`https://httpstat.us/200`)
- wg.Done()
- }()
- }
- wg.Wait()
- }
這是跟蹤:
STW 階段,系統(tǒng)調(diào)用正在結(jié)束。但是,由于沒有可用 P(如上一節(jié)所述,它們都被標(biāo)記為已停止),goroutine 將被放入全局隊(duì)列,并在世界恢復(fù)時(shí)稍后運(yùn)行。
延遲時(shí)間
“STW” 第三步涉及將所有 M 與其 P 分離。但是,Go 將等待它們自行停止:在調(diào)度程序運(yùn)行時(shí),在 syscall 調(diào)用中等。等待 goroutine 被搶占應(yīng)該很快,但是在某些情況下,可能會(huì)導(dǎo)致某些延遲。讓我們以一個(gè)極端的情況為例:
- func main() {
- var t int
- for i := 0;i < 20 ;i++ {
- go func() {
- for i := 0;i < 1000000000 ;i++ {
- t++
- }
- }()
- }
- runtime.GC()
- }
在這里,“ Stop the World”階段需要 2.6 秒:
沒有函數(shù)調(diào)用的 goroutine 將不會(huì)被搶占,并且 P 在任務(wù)結(jié)束之前不會(huì)被釋放。這將迫使“STW”等待。有幾種解決方案可改善循環(huán)中的搶占,有關(guān)此方面的更多信息,建議閱讀我另外一篇文章“ Go:Goroutine和搶占 ②”。
相關(guān)鏈接:
- https://medium.com/a-journey-with-go/go-how-does-the-garbage-collector-mark-the-memory-72cfc12c6976
- https://medium.com/a-journey-with-go/go-goroutine-and-preemption-d6bc2aa2f4b7