背景

新版发布了新的定时器实现, 声称性能优化了很多. 这里特意记录. 同时, 也偶尔看到有同事写错了相关实现, 这里补充

历史

1.10 之前, 一个独立的timerproc通过小顶堆和futexsleep 管理定时任务 1.10 ~ 1.13: 64个timerproc协程 + 四插堆 1.14: 不再有timerproc, 基于 netpoll的epoll wait来做就近时间的休眠等待, 在每次runtime.findrinnable 调度时都检查运行到期的定时器

基本结构:

  1. p 里面存储了 timers []*timer (runtime2.go), 其他辅助结构 timersLock 和 其他timer状态的计数, 使用 四插堆维护 timer, 使用锁避免插入冲突

  2. netpoller 支持 超时等待, 这样p可以利用这个特性进行timer的等待

基本流程

timer 支持 Timer.NewTimer、 Timer.Stop 和 Timer.Reset 三种接口.

下面分析三个接口

  1. Timer.NewTimer 其实就是 创建了 timer对象, 然后调用了 runtime/time.addtimer 方法, 将timer对象添加到p的timers四插堆, 需要注意的是 这里执行了两个特殊的操作: cleantimers 和 wakeNetPoller, cleantimers 是删除 timer0(最早到期的那个); wakeNetPoller 只有在 新增timer的时间比较早的时候才会触发, 个人猜想是为了 解决 fundrunnable 阻塞在timer的设计 (后面细讲).

  2. Timer.Stop 最终调用的是 runtime/time.deltimer , 需要注意的是, 其实这里并不会真正的删除, 只是标记status为 timerDeleted, 后面在 checktimers 方法中被删除 (findrunnable 和 schedule), 如果是删除的是 timers0, 那么 NewTimer 的时候也会触发删除.

  3. Timer.Reset 比较特殊, 需要考虑两种情况: 一种是 timer 已经过期被移除了, 那么 直接添加, 唤醒 netpoll. 一种是 timer还没有被移除, 则会进入modifying 状态 (记录 ajustTimers, 给后面逻辑判断), 因为 timer 不一定是这个 p 上的, modifying 如果是 改的过期时间更小, 则需要唤醒 netpoll.

那么 什么时候 modifying 被改成 正常状态并被执行? checktimers 入口会调用 runtime/time.adjusttimers, 将 modyfing 状态的 timer 添加到 timers 四插堆中, 注意是 先删除(真删除) 再添加.

内部实现

netpoller 支持

前面讲到了 netpoller 支持 timer, 这个是怎么回事呢?

首先在 findrunnable 的代码块中, 如果没有可用的 goroutine, 但是有timer的话, 会尝试阻塞, 部分代码如下:

func findrunnable() (gp *g, inheritTime bool) { // proc.go
	.....
	now, pollUntil, _ := checkTimers(_p_, 0) // 返回下一个timer到期的时间
	....
	delta := int64(-1)
	if pollUntil != 0 {
		// checkTimers ensures that polluntil > now.
		delta = pollUntil - now
	}
	......
	if netpollinited() && (atomic.Load(&netpollWaiters) > 0 || pollUntil != 0) && atomic.Xchg64(&sched.lastpoll, 0) != 0 {
		....
		list := netpoll(delta) // block until new work is available
		....
	}
}

从代码中可以发现, 存在timer的场景的, 最终会调用 netpoll(delta) 进行超时堵塞. 那么什么时候回被唤醒呢?

刚才分析到 time.addtimer 会调用 wakeNetPoller, 这里就会通过 pipe 的读写管道管道唤醒.

什么时候真正的删除

除了 addTimer 的时候会触发 timer[0] 的删除, 主要的删除逻辑在 runtime/proc.checkTimers 函数中. 在 runtime/proc.schedule 和 runtime/proc.findrunnable 触发, 可以发现还是很频繁触发的. findrunnable 除了 调用一次 checkTimers, 如果没有goroutine可用且timer存在, 会选择超时阻塞 (如果没有goroutine 没有timer 但是有 网络调用, 就会无限阻塞), 直到 addTimers 触发 netpoll 唤醒 或者 超时.

删除的代码的完整实现是 runtime/time.clearDeletedTimers , 是 runtime/proc.checkTimers 调用的. 删除会遍历所有的timer将 timerDeleted 状态的timer 删除

除此之外, runtime/time.adjusttimers 除了处理 modifying 状态的timer, 也会删除 timerDeleted 的timer 以及 modyfing 的timer (会在添加).

什么时候触发执行

这段代码 在 runtime/proc.checkTimers , 这段代码会首先执行 到期的timers, 然后在调用 runtime/time.clearDeletedTimers 删除过期的消息

timer 的状态

timer的初始状态是 timerNoStatus, 添加成功进入 timerWaiting. 整体流程如下:

timerNoStatus -> timerWaiting -> timerRunning ---即将调用执行--> timerNoStatus ---> 执行代码

如果用户主动删除, 流程如下

timerDeleted -> timerRemoving --(dodeltimer0)--> timerRemoved: 最终被gc掉

补充

  1. time.sleep 也是基于 timer 机制实现的. time.now 通过汇编实现

  2. 因为 timer 可能在其他的p 上, 所以删除操作是比较麻烦的, 实现通过 标记删除实现, 然后通过 addTimer 和 clearDeletedTimers(调度的时候触发) 执行删除

参考