摘要:但是, 我們之前提到, STW 把所有的處理器 P 都標爲停止狀態 (stopped) , 所以, 這個系統調用的 Goroutine 也會被放到全局隊列中, 等待 golang 世界恢復之後, 被重新啓用。我們來簡單分析爲什麼會出現這麼長的 STW: 正如例子中的 main 函數, 一個沒有函數調用的 Goroutine 一般不會被搶佔, 那麼這個 Goroutine 對應的處理器 P 在任務結束之前不會被釋放。

本篇文章討論實現原理基於 Go 1.13.

在垃圾回收機制 (GC) 中,"Stop the World" (STW) 是一個重要階段。 顧名思義, 在 "Stop the World" 階段, 當前運行的所有程序將被暫停, 掃描內存的 root 節點和添加寫屏障 (write barrier) 。 本篇文章討論的是, "Stop the World" 內部工作原理及我們可能會遇到的潛在風險。

Stop The World(STW)

這裏面的"停止", 指的是停止正在運行的 goroutines。 下面這段程序就執行了 "Stop the World":

func main() {
   runtime.GC()
}

這段代碼中, 調用 runtime.GC() 執行垃圾回收, 會觸發 "Stop the World"的三個步驟。

(關於關於垃圾回收機制, 可以參考我的另外一篇文章 "Go: 內存標記在垃圾回收中的實現" ):

這個階段的第一步, 是搶佔所有正在運行的 goroutine(即圖中 G ):

被搶佔之後, 這些 goroutine 會被懸停在一個相對安全的狀態。 同時,承載 Goroutine 的處理器 P (無論是正在運行代碼的處理器還是已在 idle 列表中的處理器), 都會被被標記成停止狀態 (stopped), 不再運行任何代碼:

接下來, Go 調度器 (Scheduler) 開始調度, 把每個處理器的 Marking Worker (即圖中 M ) 從各自對應的處理器 P 分離出來, 放到 idle 列表中去, 如下圖:

在停止了處理器和 Marking Worker 之後, 對於 Goroutine 本身, 他們會被放到一個全局隊列中等待:

到目前爲止, 整個"世界"被停止. 至此, 僅存的 "Stop The World" (STW)goroutine 可以開始接下來的回收工作, 在一些列的操作結束之後, 再啓動整個"世界"。

我們也可以在 Tracing 工具中看到一次 STW 的運行狀態:

系統調用

下面我們來討論一下 STW 是如何處理系統調用的。

我們知道, 系統調用是需要返回的, 那麼當整個"世界"被停止的時候, 已經存在的系統調用如何被處理呢?

我們通過一個實際例子來理解:

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()
}

這是一段簡單的系統調用的程序, 我們通過 Tracing 工具看一下它是如何被處理的:

我們可以看到, 這個系統調用 goroutine (即圖中 G30 ) 在"世界"被停止的時候, 就已經存在了。

但是, 我們之前提到, STW 把所有的處理器 P 都標爲停止狀態 (stopped) , 所以, 這個系統調用的 Goroutine 也會被放到全局隊列中, 等待 golang 世界恢復之後, 被重新啓用。

延遲

前文提到 STW 的第三步是將 Marking Worker( M ) 從處理器( P )上分離, 然後放入 idle 列表中。

而實際上, Go 會等待他們自發停止, 也就是說當調度器(scheduler)運行的時候, 系統調用在運行的時候, STW 會等待。

理論上, 等待一個 Goroutine 被搶佔是很快的, 但是在有些情況下, 還是會出現相應的延遲。

我們通過一個例子來模擬類似情況:

func main() {
   var t int
   for i := 0;i < 20 ;i++  {
      Go func() {
         for i := 0;i < 1000000000 ;i++ {
            t++
         }
      }()
   }

   runtime.GC()
}

我們還是來看一下這段代碼運行的 Tracing 情況, 從下圖我們可以看到 STW 階段總共耗時 2.6 秒:

我們來簡單分析爲什麼會出現這麼長的 STW: 正如例子中的 main 函數, 一個沒有函數調用的 Goroutine 一般不會被搶佔, 那麼這個 Goroutine 對應的處理器 P 在任務結束之前不會被釋放。

而 STW 的機制是等待它自發停止, 因此就出現了 2.6 秒的 STW。

爲了提高整體程序的效率, 我們一般需要避免或者改進這種情況。

關於這部分, 大家可以參考我的另一篇文章 "Go: Goroutine 和搶佔"

相關文章