golang NewTimer/NewTicker 源码阅读
1.引出 src/runtime/time.go 中的 startTimer
NewTimer 和 NewTicker 分别在 src/time/sleep.go、src/time/tick.go 这两个文件下。这两个函数最主要的区别是NewTimer在初始化runtimeTimer的时候没有初始化period属性。不管是 NewTimer 还是 NewTicker 最终的实现都是调用 startTimer,startTimer 在 src/time/sleep.go 下没有函数体的,它的实现是在 src/runtime/time.go 中的 startTimer,这两者是通过 go:linkname 指令关联的。
2.阅读 src/runtime/time.go 中的 startTimer
看源码可知 startTimer 调用了addtimer,而 addtimer 调用了 assignBucket 和addtimerLocked。
我们可以先看 assignBucket。从 assignBucket 的源码可知所有的 timer 对象都在一个固定大小为64的桶数组里面,每一个数组元素里面有一个桶对象 timersBucket,从 timersBucket 这个数据结构可以看出这个桶对象包含一个最小堆和一个 loop 循环协程,而 timer 对象归哪个桶管理取决于申请该 timer 对象时 G 所在的 P(通过P的id取余64作为桶数组下标)。
我们再来看 addtimerLocked,addtimerLocked 主要调用了addtimerLocked,所以我们主要看 addtimerLocked。看 addtimerLocked 这个函数的源码可知其实就是把 timer 对象加到这个timer 所属桶的 timer 对象堆里,然后最小堆根据桶内所有 timer 的超时触发绝对时间点做调整。另外,和桶一对一关联的桶协程是懒开启的,只在桶被初次使用时(即有 timer 对象 hash 到了这个桶)才开启,开启后桶协程内部的循环永远不会退出。
最后我们来看这个桶协程实现 timerproc。这里有两层循环,先来看第二层循环:不断的取最小堆里面的第一个 timer 最小对象,如果没有 timer 最小对象没有超时,则 break,退出第二层循环,进入第一层循环。如果超时了,period 大于0则根据 period 延长 when ,period 小于等于0则从这个桶的最小堆里把这个 timer 对象删除。之后然后 siftdownTimer 然后再往通道发送信息 f(arg, seq)。当桶内没 timer 时,桶协程被挂起,即 rescheduling 状态。当桶内还有 timer 时,桶内协程睡眠直到最小超时触发时间点后再唤醒,即 sleeping 状态。当往桶内加入新 timer 而该 timer 的超时触发时间点正好是当前桶内最小的,则唤醒桶协程,让桶协程重新判断,设置新的最小超时触发时间点后进入 sleeping 状态。