目录
定时器底层结构的发展过程
1. go 1.10之前,全局四叉堆
使用最小四叉堆。由于全局四叉堆共用互斥锁,锁竞争影响性能。
2. go 1.10-1.13,全局64个四叉堆
将全局四叉堆分成64个更小的四叉堆,每个处理器(P)创建的计时器由对应的四叉堆维护
这种分片方式降低了锁粒度,提高了性能,但是造成了cpu频繁上下文切换。
3. go 1.14之后,每个处理器单独管理计时器,并通过网络轮询器触发
每个P拥有一个最小四叉堆,单独管理。交给网络轮询器和调度器触发,较少上下文切换开销。
数据结构
type p struct {
timersLock mutex
timers []*timer // 每个p拥有一个timers,即存储计时器的最小四叉堆
numTimers uint32
adjustTimers uint32
deletedTimers uint32
...
}
type timer struct {
pp puintptr
when int64 //当前计时器被唤醒的时间
period int64 //两次被唤醒的间隔
f func(interface{}, uintptr) //每当计时器被唤醒时都会调用的函数
arg interface{} //计时器被唤醒时调用 f 传入的参数
seq uintptr
nextwhen int64 //计时器处于 timerModifiedXX 状态时,用于设置 when 字段
status uint32 //计时器的状态
}
对外暴露的Timer结构体:
type Timer struct {
C <-chan Time
r runtimeTimer
}
状态机
10种状态:
-
timerNoStatus:还未设置状态
-
timerWaiting:等待触发
-
timerRunning:运行计时器函数
-
timerDeleted:被删除
-
timerRemoving:正在被删除
-
timerRemoved:已经被停止并从堆中删除
-
timerModifying:正在被修改
-
timerModifiedEarlier:被修改到了更早的时间
-
timerModifiedLater:被修改到了更晚的时间
-
timerMoving:已经被修改正在被移动
动作:
-
增加计时器
-
删除计时器
-
修改计时器
-
清除计时器
-
调整计时器
-
运行计时器
计时器的触发
go会在两个模块触发计时器,运行计时器中的函数:
1. 调度器
调度器会检查计时器是否准备就绪。
运行计时器函数的时机:
- 调度器调用 runtime.schedule 执行调度时
- 调度器调用 runtime.findrunnable 获取可执行的goroutine 或从其他处理器窃取计时器时
2. 系统监控
后台系统监控(函数runtine.sysmon)会检查是否有未执行的到期计时器。
Timer和Ticker使用
1. timer
ch := make(chan int, 1)
go func() {
time.Sleep(2 * time.Second)
ch <- 1
}()
timer := time.NewTimer(1 * time.Second)
select {
case <-ch:
fmt.Println("Got response.")
case <-timer.C:
fmt.Println("Timeout.”) // 会进到这里超时
}
2. ticker
// 每隔1s执行一次
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
fmt.Println("task triggered")
}
}