前言
在1.14版本以前,go的time是会包装成一个协程执行,那么就有一个问题(极端情况),若已达协程最大数且所有协程进入死循环,之后的time就没法执行(本质上是没法产生新协程)。此外,time包实现计时器的数据结构也经历了很多变化
1.数据结构变化
1.1 全局四叉堆
在1.9之前所有的计时器由一个全局唯一的四叉堆(time heap)维护,所有的timer操作去抢这个四叉堆的同一把锁,即执行time.Sleep或After函数都会创建timer对象挂到runtime的四叉堆上。
最初的四叉堆runtime.timers结构体
type timer struct {
i int // heap index
when int64
period int64
f func(interface{}, uintptr)
arg interface{}
seq uintptr
}
var timers struct {
lock mutex
gp *g
created bool
sleeping bool
rescheduling bool
sleepUntil int64
waitnote note
t []*timer
}
- lock全局唯一锁
- t是最小四叉堆,timer都会放在其中
runtime.rimerproc创建一个Gorutinue来进行运行时间事件驱动来唤醒计时器(在for循环中不断检查堆元素是否到期,到期则触发并调整堆元素位置,直到所有timer执行完毕去休眠)
1.1.1四叉堆原理
四叉堆说白了就是四叉树,最多有4个子节点四叉堆建立基准:以timer的触发时间(timer的when属性)为准进行排列,堆顶(根)一定是最快时间触发的,来一个新timer则进行按规则调整(四叉树的父节点的触发时间一定小于子节点,四个兄弟的节点间并不要求按其触发时间早晚排序)
1.1.2为什么设计要使用4叉堆
算法弱,直接看网上结论吧:四叉堆与二叉堆比较
1.插入操作执行性能更好优于二叉堆(插入耗时只有一半)
2.上推操作更快
也就是说timer更注重插入和上推操作
1.1.3弊端
共用一把互斥锁,严重影响计时器性能
1.2分片四叉堆
1.10将全局四叉堆分成了64个更小的四叉堆(理想情况应该是四叉堆数量和处理器数量相等,这里以内存换取性能直接写死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
}
bucket这个单词在学习哈希桶的时候很熟悉了
每一个计时器桶上都由一个timerproc创建协程处理
G如何找桶?G都有一个id,找对应的桶编号即可
1.2.1弊端
将全局锁分成了粒度更小的锁,但是需要处理器频繁切换上下文,也影响了计时器性能
1.3最小四叉堆
1.14之后时间事件不再依赖timerproc(该函数被移除)创建G来执行,而是直接加入到GMP的调度循环中(交给网络轮询器和调度器触发)。
runtime.p是用来调度M处理G的p中有5个字段与time相关
type p struct {
...
timersLock mutex
timers []*timer
numTimers uint32
adjustTimers uint32
deletedTimers uint32
...
}
- timersLock互斥锁
- timers为最小四叉堆
- numTimers是处理器中的计时器数量
- adjustTimer 处理器中处于 timerModifiedEarlier 状态的计时器数量;
- deletedTimers是处理器中处于timerDeleted状态的计时器数量
schedule源码
同时它也有工作窃取,在饥饿状态下会去其它P那里偷取timer
1.3.1兜底
runtime.sysmon会给未被触发的timer兜底,启动新线程
2.timer结构体
上面的是1.14之前的,现在我的是1.18
利用函数NewTimer创建Timer
func NewTimer(d Duration) *Timer {
c := make(chan Time, 1)
t := &Timer{
C: c,
r: runtimeTimer{
when: when(d),
f: sendTime,
arg: c,
},
}
//顾名思义启动计时器,本质上是加入到四叉树中
startTimer(&t.r)
return t
}
内部创建了一个通道和Timer结构体
Timer是对外暴露的,是对私有timer的包装
time.Timer
type Timer struct {
C <-chan Time
r runtimeTimer
}
time.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 计时器处于 timerModifiedXX 状态时,用于设置 when 字段;
- status是计时器状态
Timer的channel作用:当计时器失效时,订阅计时器Channel的Goroutine会收到计时器失效信号/时间,最常见的例子就是time.After的使用
Timer的管道也是有缓存的,在结束后会调用sendTime方法往管道发送数据
func sendTime(c interface{}, seq uintptr) {
select {
case c.(chan Time) <- Now():
default:
}
}
这里因为有default的原因,即使管道满了没有接收方,也不会造成阻塞
2.1计时器状态
一共有10种状态,不同的状态记录不同的信息(存在时间和是否在堆上),这里先只分析两种
2.2增加计时器addtimer
startTimer本质上是执行addtimer
func startTimer(t *timer) {
if raceenabled {
racerelease(unsafe.Pointer(t))
}
addtimer(t)
}
time.addtimer
func addtimer(t *timer) {
...前面一堆验证
t.status = timerWaiting
when := t.when
pp := getg().m.p.ptr()
lock(&pp.timersLock)
cleantimers(pp)
doaddtimer(pp, t)
unlock(&pp.timersLock)
wakeNetPoller(when)
}
- timer的状态改为timerWaiting等待触发
- cleantimers清除处理器中的计时器
- doaddtimer将其加入四叉堆中
- 调用wakeNetPoller唤醒网络轮询器中休眠的线程
每次增加新的计时器都会中断正在阻塞的轮询,触发调度器检查是否有计时器到期
2.3清除计时器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,再调用dodeltimer0删除四叉堆顶上的计时器,之后将计时器状态修改为timerRemoved
- timerModifiedEarlier或timerModifiedLater:
先改为Removing,再使用下次触发时间nextWhen覆盖when,删除堆顶计时器,使用doaddtimer将计时器加入队中,修改timerRemoved
2.3调度timer
即checkTimers函数,它在以下情况下被调用:
- schedule执行调度
- findrunable获取可执行G
- findrunable从其他处理器窃取计时器
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)
- 如果处理器中不存在需要调整的计时器(主要指调整四叉树的节点位置,可能是遍历也可能是修改堆顶):
- 没有需要执行的计时器则直接返回
- 下一个计时器没到期且需要删除计时器较少则直接返回
- 否则调用adjusttimers调整计时器
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
}
}
通过runtimer依次检查堆中是否存在需要执行的计时器,存在则直接运行,不存在则获取罪行计时器触发时间,runtime.runtimer是运行计时器
2.4计时器兜底
即runtime.sysmon一般是以10s为周期,检查并启动线程来处理计时器(会遍历运行时的全部处理器)
3.Timer使用的一些坑
主要为错误创建过多timer浪费资源和调用Stop并不会主动关闭C,阻塞程序
func main() {
for {
// xxx 一些操作
timeout := time.After(30 * time.Second)
select {
case <- someDone:
// do something
case <-timeout:
return
}
}
}
for中不断创建timer,我们知道checkTimers会轮询遍历timer堆的,这样创建过多的timer会增加遍历耗时,内存暴涨导致CPU异常,正确做法是利用time.Reset来重复利用timer
func main() {
timer := time.NewTimer(time.Second * 5)
for {
t.Reset(time.Second * 5)
select {
case <- someDone:
// do something
case <-timer.C:
return
}
}
}
另一个Stop的例子
func main() {
timer1 := time.NewTimer(2 * time.Second)
go func() {
timer1.Stop()
}()
<-timer1.C
println("done")
}
此时尽管子协程调用stop停止了计时器,可是main仍堵塞,根本原因是通道没关闭
stop源码
func (t *Timer) Stop() bool {
if t.r.f == nil {
panic("time: Stop called on uninitialized Timer")
}
return stopTimer(&t.r)
}
只关闭了私有变量timer
func main() {
timer1 := time.NewTimer(2 * time.Second)
go func() {
if !timer1.Stop() {
<-timer1.C
}
}()
select {
case <-timer1.C:
fmt.Println("expired")
default:
}
println("done")
}
参考
1.一起学GO计时器
2.timer如何被调度
- 计时器的实现
4.计时器底层实现深度刨析
5.四叉堆与二叉堆比较