time
golang在1.14之前time改动了很多个版本,从全局到分片到网络轮询器,我们以最新的1.20.4讲解,有兴趣的同学可以看看对应的issues 我们还是先看图(//todo ASSIC不好画,后面有空在画,不影响阅读),再看具体代码
- 全局四叉堆:锁争用严重
- 分片四叉堆: 固定分割成64个hash减少锁力度,处理器和线程之间切换影响性能
- 网络轮询器版本: 所有的计时器以最小四叉堆存储在runtime.p 下面我们来看下网络轮询起中的版本
time 结构
time在1.14版本后,每个p上都挂了一个time的四叉小顶堆,结构如下,根据time上的when字段排序,具体调度触发time由schedule逻辑以及sysmon线程
结合netpoll去执行,可以看看runtime系列里schdule逻辑,以及netpoll文章

p里与time相关的字段如下
type p struct {
//堆顶最早一个执行时间
timer0When atomic.Int64
//timer修改最早时间
timerModifiedEarliest atomic.Int64
//操作timers数组的互斥锁
timersLock mutex
//四叉堆
timers []*timer
//四叉堆里timer的总数
numTimers atomic.Uint32
//标记删除的数量
deletedTimers atomic.Uint32
}
下面先简单了解下timer的具体结构,也就是四叉堆里的元素,方便后面具体调用过程
type timer struct {
//在堆上的time会保存对应p
pp puintptr
//计时器被实际唤醒时间
when int64
//周期性的定时任务两次被唤醒的间隔
period int64
//被唤醒的回调函数
f func(any, uintptr)
//回调函数传参
arg any
//回调函数的参数,在netpoll使用
seq uintptr
//处于特定状态,设置when字段
nextwhen int64
//状态
status atomic.Uint32
}
简单点来说,就是timer启动的时候,插入当前p上的四叉堆,同时p记录要执行的时间,//todo 我们在看看新建一个timer具体的流程与代码,使用dlv找到我们具体的代码为addtimer
添加定时器
// src/runtime/time.go
func addtimer(t *timer) {
t.status.Store(timerWaiting)
when := t.when
//获取当前g所在的m,会加锁处理,以及对应的p
mp := acquirem()
pp := getg().m.p.ptr()
//加锁
lock(&pp.timersLock)
//清理掉timer堆中删除的(也就是调用time.stop),以及过期的timer
cleantimers(pp)
//添加timer到堆上,主要调用堆排序算法插入,感兴趣可以自己去源码里看看
//同时将堆顶元素存到p的timer0When字段
doaddtimer(pp, t)
unlock(&pp.timersLock)
//when的值小于pollUntil时间,唤醒正在netpoll休眠的线程
wakeNetPoller(when)
releasem(mp)
}
我们根据上面代码了解了添加定时器,删除,修改调整定时器这里不在啰嗦了,就是操作四叉堆以及timer的状态机,我们重点描述下调度的触发逻辑
触发定时器
checkTimers是调度器触发timer运行的函数,主要在以下函数中触发,我们来看看具体触发逻辑
- schedule()
- findrunnable() 获取可执行的g
- stealWork()
- sysmon()
以上3个地方调用的都是checkTimers函数,该函数调整堆,删除一些比如过期的timer,调用time.stop的timer,以及返回下一个timer需要执行的时间,如果有
需要执行的则直接执行,下面看看具体代码
// src/runtime/time.go
func checkTimers(pp *p, now int64) (rnow, pollUntil int64, ran bool) {
//下一个timer的调度时间
next := pp.timer0When.Load()
//某个timer被修改,且修改的触发时间早于next
nextAdj := pp.timerModifiedEarliest.Load()
if next == 0 || (nextAdj != 0 && nextAdj < next) {
next = nextAdj
}
//没有需要调度的
if next == 0 {
return now, 0, false
}
if now == 0 {
now = nanotime()
}
//当前m不是该p绑定的,也就是说通过sysmon调度的,或者需要删除的timer小于1/4堆内元素
if now < next {
if pp != getg().m.p.ptr() || int(pp.deletedTimers.Load()) <= int(pp.numTimers.Load()/4) {
return now, next, false
}
}
lock(&pp.timersLock)
//四叉堆不为空
if len(pp.timers) > 0 {
//更新pp.timerModifiedEarliest数量
adjusttimers(pp, now)
for len(pp.timers) > 0 {
//运行计时器,主要是调整四叉堆,以及操作状态及,以及最早的timer需要运行的时间
if tw := runtimer(pp, now); tw != 0 {
if tw > 0 {
pollUntil = tw
}
break
}
ran = true
}
}
//四叉堆的维护以及删除过期定时器
if pp == getg().m.p.ptr() && int(pp.deletedTimers.Load()) > len(pp.timers)/4 {
clearDeletedTimers(pp)
}
unlock(&pp.timersLock)
//返回现在时间,以及下一个timer的运行时间,ran为是否有下一个需要运行
return now, pollUntil, ran
}
sysmon调度
调度的地方有很多,只是举例在sysmon里的调度,就是通过timeSleepUntil()函数下一个需要调度的时间
// src/runtime/proc.go
func sysmon() {
//获取pp.timer0When或者timerModifiedEarliest
next := timeSleepUntil()
if next := timeSleepUntil(); next < now {
startm(nil, false)
}