六-4:计时器

一、设计原理

Go语言的定时器经历过很多个版本迭代

  1. Go 1.9版本之前,使用全局唯一的四叉堆维护
  2. Go 1.10-1.13,全局使用64个四叉堆,每个处理器(P)对应一个四叉堆
  3. Go 1.14版本之后,每个处理器P直接管理一个四叉堆,通过网络轮询器触发
1. 全局四叉堆

所有的计时器都会存储在如下结构中

var timers struct {
	lock         mutex
	gp           *g
	created      bool
	sleeping     bool
	rescheduling bool
	sleepUntil   int64
	waitnote     note
	t            []*timer
}
  • 这个结构中的t就是最小四叉堆,运行时所有的计时器都会加入其中。
  • 在以下事件发生时会唤醒计时器
    • 四叉堆中有计时器到期
    • 四叉堆加入了触发时间更早的新计时器
  • 缺点:全局互斥锁对性能影响大
2. 分片四叉堆

将全局四叉堆分割成了64个小的四叉堆。

const timersLen = 64

var timers [timersLen]struct {
	timersBucket
}

type timersBucket struct {
	lock         mutex
	gp           *g
	created      bool
	sleeping     bool
	rescheduling bool
	sleepUntil   int64
	waitnote     note
	t            []*timer
}
  • 如果机器处理器P超过64,就会有多个处理器的计时器在同一个bucket中。每个桶有一个协程处理
  • 缺点:分片降低了锁的粒度,但处理器和线程之间频繁的上下文切换影响性能
3. 网络轮询器

四叉堆直接存储在 runtime.p中

type p struct {
	...
	timersLock mutex
	timers []*timer

	numTimers     uint32
	adjustTimers  uint32
	deletedTimers uint32
	...
}
  • timers存储计时器的最小四叉堆
  • 目前计时器都处理器的网络轮询器和调度器触发
  • 优点:能充分利用本地性,减少上下文的切换开销

二、数据结构

Go语言计时器的内部表示是runtime.timer

type timer struct {
	pp puintptr

	when     int64
	period   int64
	f        func(interface{}, uintptr)
	arg      interface{}
	seq      uintptr
	nextwhen int64
	status   uint32
}
  • when:当前计时器被唤醒的时间
  • period:两次被唤醒的间隔
  • f:每当计时器被唤醒时调用的函数
  • arg:执行f时传入的参数
  • nextWhen:计时器处于timeModifiedXX状态时,用于设置when字段
  • status:计时器的状态

Go语言对外暴露的计时器结构是time.Timer

type Timer struct {
	C <-chan Time
	r runtimeTimer
}

三、状态机

runtime使用状态机的方式处理全部定时器

状态解释
timerNoStatus还没有设置状态
timerWaiting等待触发
timerRuning运行计时器函数
timerDelete被删除
timerRemoving正在被删除
timerRemoved已经被停止并从堆中删除
timerModifying正在被修改
timerModifiedEarlier被修改到了更早的时间
timeerModifiedLater被修改到了更晚的时间
timerMoving已经被修改正在被移动
1. 增加计时器

增加计时器会调用runtime.addtimer函数

func addtimer(t *timer) {
	if t.status != timerNoStatus {
		badTimer()
	}
	t.status = timerWaiting
	cleantimers(pp)
	doaddtimer(pp, t)
	wakeNetPoller(when)
}
  • 状态:timerNoStatus -> timerWaiting
  • 清理处理器中的计时器
  • 将当前计时器加入处理器的timer四叉堆中
  • 唤醒网络轮询器中休眠的线程

每次增加新的计时器都会中断正在阻塞的计时器,触发调度器检查是否有计时器到期

2. 删除计时器

可能会遇到删除其他处理器的计时器的情况。删除只是将计时器状态标记为删除状态,然后由持久计时器的处理器完成删除工作

3. 修改计时器

修改计时器会调用runtime.modtimer函数

func modtimer(t *timer, when, period int64, f func(interface{}, uintptr), arg interface{}, seq uintptr) bool {
	status := uint32(timerNoStatus)
	wasRemoved := false
loop:
	for {
		switch status = atomic.Load(&t.status); status {
			...
		}
	}

	t.period = period
	t.f = f
	t.arg = arg
	t.seq = seq

	if wasRemoved {
		t.when = when
		doaddtimer(pp, t)
		wakeNetPoller(when)
	} else {
		t.nextwhen = when
		newStatus := uint32(timerModifiedLater)
		if when < t.when {
			newStatus = timerModifiedEarlier
		}
		...
		if newStatus == timerModifiedEarlier {
			wakeNetPoller(when)
		}
	}
}
  • 如果被修改的计时器已经被删除,则会调用runtime.doaddtimer创建新的计时器
  • 如果修改后的时间大于或等于修改时间,设置计时器的状态为timerModifyLater
  • 如果修改后的时间小于修改前时间,设置计时器状态为timerModifyEarlier并触发调度器重新调度
4. 清除计时器

runtime.cleantimers函数会根据状态清理处理器队列头中的计时器

func cleantimers(pp *p) bool {
	for {
		if len(pp.timers) == 0 {
			return true
		}
		t := pp.timers[0]
		switch s := atomic.Load(&t.status); s {
		case timerDeleted:
			atomic.Cas(&t.status, s, timerRemoving)
			dodeltimer0(pp)
			atomic.Cas(&t.status, timerRemoving, timerRemoved)
		case timerModifiedEarlier, timerModifiedLater:
			atomic.Cas(&t.status, s, timerMoving)

			t.when = t.nextwhen

			dodeltimer0(pp)
			doaddtimer(pp, t)
			atomic.Cas(&t.status, timerMoving, timerWaiting)
		default:
			return true
		}
	}
}
  • 如果计时器状态为timerDeleted
    • 将计时器的状态修改成timerRemoving
    • 删除四叉堆顶上的计时器
    • 将计时器状态修改为timerRemoved
  • 如果计时器的状态为timerModifiedEarlier,timerModifiedLater
    • 将计时器状态修改为timerMoving
    • 使用计时器下次触发的时间nextWhen覆盖when
    • 删除四叉堆顶上的计时器
    • 将计时器加入四叉堆中
    • 将计时器的状态修改成timerWaiting
5. 调整计时器

类似清除计时器,都会删除堆中的计时器,并修改状态为timerModifiedEarliertimerModifiedLater。不同的是,调整计时器会遍历处理器堆中的所有计时器

func adjusttimers(pp *p, now int64) {
	var moved []*timer
loop:
	for i := 0; i < len(pp.timers); i++ {
		t := pp.timers[i]
		switch s := atomic.Load(&t.status); s {
		case timerDeleted:
			// 删除堆中的计时器
		case timerModifiedEarlier, timerModifiedLater:
			// 修改计时器的时间
		case ...
		}
	}
	if len(moved) > 0 {
		addAdjustedTimers(pp, moved)
	}
}
6. 运行计时器

runtime.runtimer会检查处理器四叉堆上最顶上的计时器,该函数也会处理计时器的删除以及计时器的更新

func runtimer(pp *p, now int64) int64 {
	for {
		t := pp.timers[0]
		switch s := atomic.Load(&t.status); s {
		case timerWaiting:
			if t.when > now {
				return t.when
			}
			atomic.Cas(&t.status, s, timerRunning)
			runOneTimer(pp, t, now)
			return 0
		case timerDeleted:
			// 删除计时器
		case timerModifiedEarlier, timerModifiedLater:
			// 修改计时器的时间
		case ...
		}
	}
}

如果处理器四叉堆顶部的计时器没有到触发事件就会直接返回,否则调用runtime.runOneTimer运行堆顶的计时器

func runOneTimer(pp *p, t *timer, now int64) {
	f := t.f
	arg := t.arg
	seq := t.seq

	if t.period > 0 {
		delta := t.when - now
		t.when += t.period * (1 + -delta/t.period)
		siftdownTimer(pp.timers, 0)
		atomic.Cas(&t.status, timerRunning, timerWaiting)
		updateTimer0When(pp)
	} else {
		dodeltimer0(pp)
		atomic.Cas(&t.status, timerRunning, timerNoStatus)
	}

	unlock(&pp.timersLock)
	f(arg, seq)
	lock(&pp.timersLock)
}
  • 如果period字段大于0
    • 修改计时器下一次触发的时间并更新其在堆中的位置
    • 将计时器的状态更新至timerWaiting
    • 设置处理器的timer0When字段
  • 如果period小于0
    • 删除计时器
    • 将计时器状态更新至timerNoStatus

四、触发计时器

Go的计时器会在以下两个模块触发

  • 调度器调度时会检查处理器中的计时器是否准备就绪
  • 系统监控会检查是否有未执行的到期计数器
1. 调度器

调度器使用runtime.checkTimers来运行处理器的计时器

func checkTimers(pp *p, now int64) (rnow, pollUntil int64, ran bool) {
	// 调整计时器
  if atomic.Load(&pp.adjustTimers) == 0 {
		next := int64(atomic.Load64(&pp.timer0When))
		if next == 0 {
			return now, 0, false
		}
		if now == 0 {
			now = nanotime()
		}
		if now < next {
			if pp != getg().m.p.ptr() || int(atomic.Load(&pp.deletedTimers)) <= int(atomic.Load(&pp.numTimers)/4) {
				return now, next, false
			}
		}
	}

	lock(&pp.timersLock)
	adjusttimers(pp)
  
  // 运行计时器
  rnow = now
	if len(pp.timers) > 0 {
		if rnow == 0 {
			rnow = nanotime()
		}
		for len(pp.timers) > 0 {
			if tw := runtimer(pp, rnow); tw != 0 {
				if tw > 0 {
					pollUntil = tw
				}
				break
			}
			ran = true
		}
	}
  
  // 删除计时器
  if pp == getg().m.p.ptr() && int(atomic.Load(&pp.deletedTimers)) > len(pp.timers)/4 {
		clearDeletedTimers(pp)
	}

	unlock(&pp.timersLock)
	return rnow, pollUntil, ran
  • 调整计时器
    • 如果处理器中不存在需要调整的计时器
      • 当没有需要执行的计时器时直接返回
      • 当下一个计时器没有到期并且需要删除的计时器较少(不到总数四分之一)时直接返回
    • 如果处理器中存在需要调整的计时器,就调用runtime.adjusttimers
  • 运行计时器
    • 如果存在需要执行的计时器,直接运行
    • 如果不存在,获取最新计时器的触发事件
  • 删除计时器
    • 如果当前goroutine的处理器P与传入的处理器相同,且处理器中需要删除的计时器是堆中计时器的1/4以上,就会删除需要删除的计时器
2. 系统监控

系统监控函数也可能会触发函数的计时器

func sysmon() {
	...
	for {
		...
		now := nanotime()
		next, _ := timeSleepUntil()
		...
		lastpoll := int64(atomic.Load64(&sched.lastpoll))
		if netpollinited() && lastpoll != 0 && lastpoll+10*1000*1000 < now {
			atomic.Cas64(&sched.lastpoll, uint64(lastpoll), uint64(now))
			list := netpoll(0)
			if !list.empty() {
				incidlelocked(-1)
				injectglist(&list)
				incidlelocked(1)
			}
		}
		if next < now {
			startm(nil, false)
		}
		...
}
  • 获取计时器的到期时间以及持有该计时器的堆
    • 此处会遍历所有计时器并查找下一个需要执行的计时器
  • 如果超过10ms没有轮询,就出发网络轮询
  • 如果当前有计时器未执行,且处理器无法被抢占,此时应该启动新的线程处理计时器
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值