在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
前言一次性定时器Timer和周期性定时器Ticker,这两种定时器内部实现机制完全相同。创建定时器的协程并不负责计时,而是把任务交给系统协程,系统协程统一处理所有的定时器。 定时器存储timer数据结构
runtimeTimer类型是time包的名称,在runtime包中,这个类型叫做timer。 timer数据结构如下所示:
其中timersBucket便是系统协程存储timer的容器,里面有个切片来存储timer,而i便是timer所在切片的下标。 timersBucket数据结构timersBucket数据结构:
系统协程在首次创建定时器时创建,定时器存储在切片中,系统协程负责计时并维护这个切片。 存储拓扑以Ticker为例,我们回顾一下Ticker、timer和timersBucket关系,假设我们已经创建了3个Ticker,那么它们之间的关系如下: 用户创建Ticker时会生成一个timer,这个timer指向timersBucket,timersBucket记录timer的指针。 timersBucket数组通过timersBucket数据结构可以看到,系统协程负责计时并维护其中的多个timer,一个timersBucket包含一个系统协程。 当系统中定时器非常多时,一个系统协程可能处理能力跟不上,所以Go在实现时实际上提供了多个timersBucket,也就有多个系统协程来处理定时器。 最理想的情况,应该预留GOMAXPROCS个timersBucket,以便充分使用CPU资源,但需要根据实际环境动态分配。为了实现简单,Go在实现时预留了64个timersBucket,绝大部分场景下这些足够了。 每当协程创建定时器时,使用协程所属的ProcessID%64来计算定时器存入的timersBucket。 下图三个协程创建定时器时,定时器分布如下图所示: 为描述方便,上图中3个协程均分布于3个Process中。 一般情况下,同一个Process的协程创建的定时器分布于同一个timersBucket中,只有当GOMAXPROCS大于64时才会出现多个Process分布于同一个timersBucket中。 定时器运行机制创建定时器创建Timer或Ticker实际上分为两步:
创建管道的部分前面已做过介绍,这里我们重点关注timer的启动部分。 首先,每个timer都必须要归属于某个timersBucket的,所以第一步是先选择一个timersBucket,选择的算法很简单,将当前协程所属的Processor ID 与timersBucket数组长度求模,结果就是timersBucket数组的下标。
至此,第一步,给当前的timer选择一个timersBucket已经完成。 其次,每个timer都必须要加入到timersBucket中。前面我们知道,timersBucket中切片中保存着timer的指针,新加入的timer并不是按加入时间顺序存储的,而是把timer按照触发的时间排序的一个小头堆。那么timer加入timersBucket的过程实际上也是堆排序的过程,只不过这个排序是指的是新加元素后的堆调整过程。 源码src/runtime/time.go:addtimerLocked()函数负责添加timer:
根据注释来理解上面的代码比较简单,这里附加几点说明:
下图展示一个小顶堆结构,图中每个圆圈代表一个timer,圆圈中的数字代表距离触发事件的秒数,圆圈外的数字代表其在切片中的下标。其中timer 15是新加入的,加入后它被最终调整到数组的1号下标。 上图展示的是二叉堆,实际上Go实现时使用的是四叉堆,使用四叉堆的好处是堆的高度降低,堆调整时更快。 删除定时器imerproc为系统协程的具体实现。它是在首次创建定时器创建并启动的,一旦启动永不销毁。 某个timer的事件触发后,根据其是否是周期性定时器来决定将其删除还是修改时间后重新加入堆。 如果堆中已没有事件需要触发,则系统协程将进入暂停态,也可认为是无限时睡眠,直到有新的timer加入才会被唤醒。 timerproc处理事件的流程图如下: 资源泄露问题前面介绍Ticker时格外提醒不使用的Ticker需要显式地Stop(),否则会产生资源泄露。研究过timer实现机制后,可以很好的解释这个问题了。 首先,创建Ticker的协程并不负责计时,只负责从Ticker的管道中获取事件; 如果创建了Ticker,则系统协程将持续监控该Ticker的timer,定期触发事件。如果Ticker不再使用且没有Stop(),那么系统协程负担会越来越重,最终将消耗大量的CPU资源 |
请发表评论