程序员社区

Go计时器的生命周期

【译文】原文地址

Go计时器的生命周期插图
Illustration created for “A Journey With Go”, made from the original Go Gopher, created by Renee French.

说明本文基于Go1.14

计时器在执行代码时是很有用的。Go在内部会对计时器的创建和执行计划进行管理。后者有点复杂的,因为Go调度器是一个协作调度器,指的是Goroutine必须自己停止(被channel阻塞、系统调用)或者在某个点被调度器暂停。

更多关于抢占式可以阅读Go协程和抢占式调度

生命周期

下面是一个计时器的简单实例:

Go计时器的生命周期插图1

当一个计时器被创建时,会被保存到一个链接到当前P(process逻辑cpu包含线程上下文)的内部列表中。下面是前面代码的一个表示:

Go计时器的生命周期插图2

关于Go调度的GMP模型可以阅读Go: Goroutine, OS Thread and CPU Management

正如你在上图看到的,计时器被创建后,会注册一个内部回调函数,这个回调函数将使用Go关键词将其转化为单独的Goroutine并发执行。然后,调度器就会对计时器进行管理。在每一轮的调度中,它都会检查计时器是否到期准备运行,如果到期的话就准备运行。实际上,因为Go调度程序本身不会执行所有的代码,计时器回调函数将加入到本地调取队列中。调度器根据调度机制来找到计时器对应的gouroutine进行调度并执行。如下所示:

Go计时器的生命周期插图3

根据本地调度队列的大小,计时器的执行可能会有一定延时。实际上,Go1.14中具有异步抢占功能,Goroutine在运行10ms后被抢占,这降低了延时的概率。

延时

为了理解延时的可能性,让我们从一个Goroutine创建大量的计时器来分析下。由于计时器会链接到当前的P(逻辑cpu)一个处于繁忙的P将无法立即执行链接到的计时器。如下程序,创建了数百个计时器,并使当前goroutine保持忙碌状态:

Go计时器的生命周期插图4

下图可以看到goroutine占用处理器的跟踪轨迹:

Go计时器的生命周期插图5

由于异步抢占,正在执行的goroutine被划分成大量小块。在这些块中,有一个看起来比其他的更大。我们放大看下:

Go计时器的生命周期插图6

当计时器必须必须运行时,就出现这种空隙。此时,当前的goroutine已被抢占,并取代Go调度程序。调度程序将计时器回调函数转换成可运行的goroutines,正如我们在图片中看到的那样。然而,当前线程的Go调度程序并不是唯一运行计时器的程序。当前的P如果一直忙碌的话,Go实现了计时器窃取策略来确保计时器能被其他P执行。异步抢占,一般都很少发生,但是在前面的例子中由于计时器数量比较大,确实发生了抢占,如下图所示:

Go计时器的生命周期插图7

如果不考虑计时器窃取,将会发生如下情况:

Go计时器的生命周期插图8

所有持有计时器的goroutines都被添加到本地队列。然后,根据工作窃取分配到其他的P上执行。
可以阅读Go: Work-Stealing in Go Scheduler,了解更多关于Go工作窃取策略。

由于异步抢占和工作窃取,延迟不太可能发生。

赞(0) 打赏
未经允许不得转载:IDEA激活码 » Go计时器的生命周期

一个分享Java & Python知识的社区