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文章 time.png

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)
	}