摘要

在 Go 里有很多种定时器的使用方法,像常规的 Timer、Ticker 对象,以及经常会看到的 time.After(d Duration) 和 time.Sleep(d Duration) 方法,今天将会介绍它们的使用方法以及会对它们的底层源码进行分析,以便于在更好的场景中使用定时器。

Go 里的定时器

我们先来看看 Timer 对象 以及 time.After 方法,它们都有点偏一次使用的特性。对于 Timer 来说,使用完后还可以再次启用它,只需要调用它的 Reset 方法。

从上面可以看出来 Timer 允许再次被启用,而 time.After 返回的是一个 channel,将不可复用。

而且需要注意的是 time.After 本质上是创建了一个新的 Timer 结构体,只不过暴露出去的是结构体里的 channel 字段而已。

for{...}

看完了有着 “一次特性” 的定时器,接下来我们来看看按一定时间间隔重复执行任务的定时器:

这里的 Ticker 跟 Timer 的不同之处,就在于 Ticker 时间达到后不需要人为调用 Reset 方法,会自动续期。

除了上面的定时器外,Go 里的 time.Sleep 也起到了类似一次性使用的定时功能。只不过 time.Sleep 使用了系统调用。而像上面的定时器更多的是靠 Go 的调度行为来实现。

实现原理

当我们通过 NewTimer、NewTicker 等方法创建定时器时,返回的是一个 Timer 对象。这个对象里有一个 runtimeTimer 字段的结构体,它在最后会被编译成 src/runtime/time.go 里的 timer 结构体。

而这个 timer 结构体就是真正有着定时处理逻辑的结构体。

一开始,timer 会被分配到一个全局的 timersBucket 时间桶。每当有 timer 被创建出来时,就会被分配到对应的时间桶里了。

为了不让所有的 timer 都集中到一个时间桶里,Go 会创建 64 个这样的时间桶,然后根据 当前 timer 所在的 Goroutine 的 P 的 id 去哈希到某个桶上:

接着 timersBucket 时间桶将会对这些 timer 进行一个最小堆的维护,每次会挑选出时间最快要达到的 timer。

如果挑选出来的 timer 时间还没到,那就会进行 sleep 休眠。

如果 timer 的时间到了,则执行 timer 上的函数,并且往 timer 的 channel 字段发送数据,以此来通知 timer 所在的 goroutine。

源码分析

上面提及了下定时器的原理,现在我们来好好看一下定时器 timer 的源码。

首先,定时器创建时,会调用 startTimer 方法:

而 addtimer 也就是我们刚刚所说的分配到某个桶的动作:

addtimerLocked 里包含了最终的时间处理函数: timerproc,重点分析下:

在上面的代码中,发现当时间桶里已经没有定时器的时候,goroutine 会调用 gopark 去休眠,直到又有新的 timer 添加到时间桶,才重新唤起执行定时器的循环代码。

另外,当堆排序挑选出来的定时器时间还没到的话,则会调用 notetsleepg 来休眠,等到休眠时间达到后重新被唤起。

总结

Go 的定时器采用了堆排序来挑选最近的 timer,并且会往 timer 的 channel 字段发送数据,以便通知对应的 goroutine 继续往下执行。

这就是定时器的基础原理了,其他流程也只是休眠唤起的执行罢了,希望此篇能帮助到大家对 Go 定时器的理解!!!


感兴趣的朋友可以搜一搜公众号「 阅新技术 」,关注更多的推送文章。
可以的话,就顺便点个赞、留个言、分享下,感谢各位支持!
阅新技术,阅读更多的新知识。