Go 语言如何实现垃圾回收中的 Stop the World (STW)

栏目: IT技术 · 发布时间: 4年前

内容简介:本篇文章讨论实现原理基于 Go 1.13.在垃圾回收机制 (GC) 中,"Stop the World" (STW) 是一个重要阶段。 顾名思义, 在 "Stop the World" 阶段, 当前运行的所有程序将被暂停, 扫描内存的 root 节点和添加写屏障 (write barrier) 。 本篇文章讨论的是, "Stop the World" 内部工作原理及我们可能会遇到的潜在风险。

Go 语言如何实现垃圾回收中的 Stop the World (STW)

本篇文章讨论实现原理基于 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 ):

Go 语言如何实现垃圾回收中的 Stop the World (STW)

被抢占之后, 这些 goroutine 会被悬停在一个相对安全的状态。 同时,承载 Goroutine 的处理器 P (无论是正在运行代码的处理器还是已在 idle 列表中的处理器), 都会被被标记成停止状态 (stopped), 不再运行任何代码:

Go 语言如何实现垃圾回收中的 Stop the World (STW)

接下来, Go 调度器 (Scheduler) 开始调度, 把每个处理器的 Marking Worker (即图中 M ) 从各自对应的处理器 P 分离出来, 放到 idle 列表中去, 如下图:

Go 语言如何实现垃圾回收中的 Stop the World (STW)

在停止了处理器和 Marking Worker 之后, 对于 Goroutine 本身, 他们会被放到一个全局队列中等待:

Go 语言如何实现垃圾回收中的 Stop the World (STW)

到目前为止, 整个"世界"被停止. 至此, 仅存的 "Stop The World" (STW)goroutine 可以开始接下来的回收工作, 在一些列的操作结束之后, 再启动整个"世界"。

我们也可以在 Tracing 工具中看到一次 STW 的运行状态:

Go 语言如何实现垃圾回收中的 Stop the World (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 工具看一下它是如何被处理的:

Go 语言如何实现垃圾回收中的 Stop the World (STW)

我们可以看到, 这个系统调用 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 秒:

Go 语言如何实现垃圾回收中的 Stop the World (STW)

我们来简单分析为什么会出现这么长的 STW: 正如例子中的 main 函数, 一个没有函数调用的 Goroutine 一般不会被抢占, 那么这个 Goroutine 对应的处理器 P 在任务结束之前不会被释放。

而 STW 的机制是等待它自发停止, 因此就出现了 2.6 秒的 STW。

为了提高整体程序的效率, 我们一般需要避免或者改进这种情况。

关于这部分, 大家可以参考我的另一篇文章 "Go: Goroutine 和抢占"


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

The Elements of Statistical Learning

The Elements of Statistical Learning

Trevor Hastie、Robert Tibshirani、Jerome Friedman / Springer / 2009-10-1 / GBP 62.99

During the past decade there has been an explosion in computation and information technology. With it have come vast amounts of data in a variety of fields such as medicine, biology, finance, and mark......一起来看看 《The Elements of Statistical Learning》 这本书的介绍吧!

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具