一、设计原理
Go语言的定时器经历过很多个版本迭代
- Go 1.9版本之前,使用全局唯一的四叉堆维护
- Go 1.10-1.13,全局使用64个四叉堆,每个处理器(P)对应一个四叉堆
- 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. 调整计时器
类似清除计时器,都会删除堆中的计时器,并修改状态为timerModifiedEarlier和
timerModifiedLater。不同的是,调整计时器会遍历处理器堆中的所有计时器
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没有轮询,就出发网络轮询
- 如果当前有计时器未执行,且处理器无法被抢占,此时应该启动新的线程处理计时器