golang 定时器

golang有timer和ticker两类定时器。timer是你想要在未来做一次某事,ticker则是在规律的间隔持续做某事。

下面让我们来看一个使用timer的例子

for {
  select {
    case <- time.After(3 * time.Second):
    	// do sth repeatly
  }
}

然而这样使用却会造成资源浪费,让我们深入源码探探究竟吧!

演化历史

无论NewTimer、time.After,还是timer.AfterFun初始化timer,都会最终加入到全局timer堆中,由go runtime统一管理

  • Go1.9之前,全局由唯一四叉堆维护,提供专门的协程timerproc管理这些计时器,协程之间竞争激烈
  • Go1.10~1.13全局使用64个四叉堆维护全部的计时器。虽然降低了锁的粒度,一定程度上解决了唯一四叉堆锁争用问题,可是唤醒timerproc带来的上下文切换问题,仍然悬而未决
  • Go1.14之后,每个P单独维护一个四叉堆,并通过网络轮询器触发

go1.14前

在go1.14前,使用64个最小堆。所有在运行中创建的timer都被添加到最小堆,每个P创建的timer由相应最小堆维护

golang 定时器_第1张图片

也就是说若P数量超过64,多个P的timer就会储存到相同bucket。

每个bucket负责管理一组这些有序timer,每个bucket都有一个相应timerproc异步任务负责调度这些定时器。

timerproc一直从bukcet取顶元素,在时间到的时候执行。在没有任务的时候,就会挂起,直到新的timer添加bucket。

timerproc在休眠时调用notesleepg,这会引发entersyscallblock调用,后者主动调用handoffp解除M、P绑定。而在下一个timer主动来临时,M和P又再次绑定。处理器P和线程M之间频繁的上下文切换也是定时器性能的主要影响因素之一。

四叉堆原理

四叉堆为四叉树。go runtime调度timer时,触发时间更早的timer,要减少查询,所以父节点触发时间是小于子节点的,并且为了同时兼顾四叉树插入、删除、重排速度,所以兄弟节点之间不要求按触发时间排序

以下是四叉堆的插入、删除动画演示

N叉堆 vs 二叉堆

  • N叉堆对缓存更友好,二叉堆则是会有更多的缓存未命中情况和虚拟内存丢失
  • N叉堆上推节点操作更快,只是二叉堆 l o g N 2 log_N 2 logN2
  • 不过,二叉堆对于入队、出队频繁的操作更合适

数据结构

状态

const (
	// 尚未设置的状态
	timerNoStatus = iota

	// 等待timer去触发,已经在一些P的堆上
	timerWaiting

	// timer正在执行
	timerRunning

	// timer被标记删除,应该被从堆中移除
	timerDeleted

	// timer正在被移除
	timerRemoving

	// timer已经停止,不在P堆
	timerRemoved

	// timer正在修改
	timerModifying

	// timer已经修改为更早的时间。新when值在nextwhen上
  // timer在某些P堆上,可能在错误的位置
	timerModifiedEarlier

  // timer已经修改为更晚或者相同的时间。新when值在nextwhen上
  // timer在某些P堆上,可能在错误的位置
	timerModifiedLater

  // timer已经被修改,正在被移动
	timerMoving
)

timer

type timer struct {
	pp       uintptr                    // 对应当前P的指针
	when     int64                      // 需要执行的时间
	period   int64                      // 周期。ticker使用
	f        func(interface{}, uintptr) // 定时任务
	arg      interface{}                // 与f相关的第一个参数
	seq      uintptr                    // 与f相关的第二个参数
	nextwhen int64                      // 下次执行的时间
	status   uint32                     // 当前状态
}

操作

加入堆过程

  1. 通过NewTimer、time.After、timer.AfterFun初始化timer后,相关timer就会放到对应P的timer堆上
  2. timer执行完毕后标记为timeRemoved
  3. 调用time.Reset(d),就会重新加入到p的timer堆上
  4. STW,runtime释放不再使用的p资源,此时将有效timer(timerWaiting、timeModifiedEarlier、timerModifiedLater)重新加入到新p的timer

runtime/timer.go#addtimer()具体放入堆过程如下

  1. 边界、状态判断

    timer被唤醒的时间when必须为正数、间隔period不能为负数。一个负数when会导致runtimer计算增值期间溢出,并且永远不会使其他运行时timer过期,而零将导致checkTimers注意不到计数器

  2. 禁止抢占,以防止改变其他P的堆

  3. 清理timer队列头部。这加快了程序创建、删除timer的速度

  4. 添加timer到当前p堆,初始化网络轮询器

  5. 唤醒网络轮询器中正在休眠的线程,若是它不打算在when之前唤醒或者还没有网络轮询器,则唤醒空闲P来访问timer和网络轮询器

加入堆的过程也可以解释前文提到为什么会资源浪费。因为time.After()实际创建了很多不需要的timer

为了解决这个问题,我们可以使用time.Reset重置timer,重复利用timer

timer :=  time.NewTimer(3 * time.Second)
for {
  select {
    case <- timer.C:
    	// do sth repeatly
  }
  timer.Reset(3 * time.Second)
}

从堆中删除

runtime/timer.go#deltimer()删除的timer,可能在其他P上,因此不能直接从timer堆删除,只是标记为已删除,然后在适当的时候被堆所在P删除

  • 对于等待去触发、被修改为晚点时间的状态、被修改为早点时间的状态,在cas修改状态为正在修改的情况下,将状态修改为deleted

  • 对于标记删除、正在一处、已经移除以及尚未设置的状态,返回timer已经执行

  • 对于正在执行、正在修改或者正在移除的状态等待完成

触发过程是怎样

在以下两种场景下会触发timer,运行timer保存的函数

  • 调度器调度时会检查P中的timer是否准备就绪
  • 系统监控会检查是否有未执行的到期timer

调度器调度执行runtime/proc.go#checkTimers运行P准备好的timer。

  1. 当没有需要执行、调整timer或者下一个计时器没有到期且需要删除timer不多于1/4,都会直接返回
  2. 当没有需要调整timer,就会调用runtime.adjusttimers根据时间将timers slice重新排列
  3. 查找并执行堆中需要执行timer。若执行成功,返回;若没有需要执行的timer,返回最近timer触发时间
  4. 在当前P和传入P为同一个,且要删除timer占1/4以上,就会调用runtime.clearDeletedTimers进行清理

reset如何操作的

runtime/time.go#resettimer()是将timer重新加入到timer堆中,等待被触发

  • 对于timerRemoved的timer,会重新设置被触发时间,加入到timer堆中
  • 对于等待被触发的timer,会修改触发时间和状态(timerModifiedEarlier或timerModifiedLater),然后由GMP触发,由checkTimers调用adjustTimers或者runtimer执行,重新加入到timer堆

stop如何执行

stop就是为了让timer停止,不再被触发。runtime/time.go#stoptimer()标记修改状态为timerDeleted,然后等待GMP触发,由checkTimers调用adjustTimers或者runtimer执行

Ref

  1. https://gobyexample.com/tickers
  2. https://juejin.cn/post/6884914839308533774
  3. https://cloud.tencent.com/developer/article/1840257
  4. https://www.sobyte.net/post/2022-01/go-timer-analysis/
  5. https://github.com/golang/go/issues/27707
  6. https://en.wikipedia.org/wiki/D-ary_heap
  7. https://segmentfault.com/a/1190000041591720
  8. https://zhuanlan.zhihu.com/p/430429302
  9. golang 1.17 source code

你可能感兴趣的:(golang,开发语言,后端)