前言
- 在go1.14 版本中,首先把存放定时事件的四叉堆放到p结构中,使用netpoll的epoll wait来做就近时间的休眠等待。在每次runtime.schedule调度时都检查运行到期的定时器。
大概使用
有时候我们会在开发中会使用到time.NewTicker
或者time.NewTimer
进行定时或者延时的处理,两者的底层实现基本是一样的,我们可以先来看看Timer
的大概使用方式
import (
"fmt"
"time"
)
func main() {
timer := time.NewTimer(2 * time.Seconds)
<-timer.C
fmt.Println("延时2s打印")
}
Timer的底层实现
我们先关注一下time.NewTimer
,他在time/sleep.go
里面
大概过程就是创建一个Timer
对象,调用startTimer
启动timer
type Timer struct {
C <-chan Time
r runtimeTimer
}
type runtimeTimer struct {
pp uintptr
when int64 // 定时器被唤醒的时间
period int64 // 两次被唤醒的讲个
f func(interface{}, uintptr) // 被唤醒之后调用的函数
arg interface{}
seq uintptr
nextwhen int64
status uint32
}
// 当定时器失效时,失效的时间就会被发送给当前定时器持有的 Channel C,订阅管道中消息的 Goroutine 就会收到当前定时器失效的时间。
- 另一个用于创建 Timer 的方法 AfterFunc 其实也提供了非常相似的结构,与 NewTimer 方法不同的是该方法没有创建一个用于通知触发时间的 Channel,它只会在定时器到期时调用传入的方法
func AfterFunc(d Duration, f func()) *Timer {
t := &Timer{
r: runtimeTimer{
when: when(d),
f: goFunc,
arg: f,
},
}
startTimer(&t.r)
return t
}
startTimer
通过link做方法映射,time/sleep.go
里调用的time.startTimer
其实是runtime
包里的。
func startTimer(t *timer) {
addtimer(t)
}
addtimer
// 把定时任务放到当前g关联的P里。
func addtimer(t *timer) {
if t.when < 0 {
t.when = maxWhen
}
t.status = timerWaiting // 状态为等待中
addInitializedTimer(t)
}
// ------------------
// 加锁来清理任务,并且增加定时任务,最后根据时间就近来唤醒netpoll
func addInitializedTimer(t *timer) {
when := t.when
pp := getg().m.p.ptr()
lock(&pp.timersLock)
ok := cleantimers(pp) && doaddtimer(pp, t)
//cleantimers(pp)就是删除第一个定时任务,其实ticker其实就是在回调函数中再把这个任务塞回去。
unlock(&pp.timersLock)
if !ok {
badTimer()
}
wakeNetPoller(when)
}
//
//当新添加的定时任务when小于netpoll等待的时间,那么wakeNetPoller会激活NetPoll的等待。
// 激活的方法很简单,在findrunnable里的最后会使用超时阻塞的方法调用epollwait,这样既可监控了epfd红黑树上的fd,又可兼顾最近的定时任务的等待。
// 唤醒正在netpoll休眠的线程,前提是when的值小于pollUntil时间。
func wakeNetPoller(when int64) {
if atomic.Load64(&sched.lastpoll) == 0 {
pollerPollUntil := int64(atomic.Load64(&sched.pollUntil))
if pollerPollUntil == 0 || pollerPollUntil > when {
netpollBreak()
}
}
}
sleep 的实现
我们通常使用 time.Sleep(1 * time.Second)
来将goroutine
暂时休眠一段时间。sleep 操作在底层实现也是基于timer
实现的。代码在runtime/time.go
- 如果完全用定时器来代替Sleep的话会损耗性能
timer := time.NewTimer(2 * time.Seconds)
<-timer.C
- 每次调用 sleep 的时候,都要创建一个 timer 对象。
- 需要一个 channel 来传递事件。
所以go用另一种方式来做:
- 每个
goroutine
底层的 G 对象上,都有一个timer
属性,这是个runtimeTimer
对象,专门给sleep
使用。当第一次调用sleep
的时候,会创建这个runtimeTimer
,之后sleep
的时候会一直复用这个timer
对象。 - 调用
sleep
时候,触发timer
后,直接调用gopark
,将当前goroutine
挂起。 - 调用
callback
的时候,直接调goready
唤醒被挂起的goroutine
。