GO Timer 机制探究
发表于2019 年 2 月 6 日由 bruceding
本文介绍 Timer
, Tick
, Sleep
的实现机制。版本是 GO 1.9 。
Ticker
每隔 duration
时间会把当前的时间点放入到 channel
中,应用可以从 channel
进行读取。应用需要周期性的时间间隔,可以使用此方法。
使用 Ticker
有两种方式, NewTicker
可以获取 Ticker
实例, Stop
可以显示的停止 Tick
运行。 Stop
可以释放 timer
资源,但不会关闭 channel
,防止应用层报错。
如果 Ticker
一直随应用运行,不会关闭,可以使用 time.Tick
直接获取 time channel
。这个没有 Ticker
实例,无法显示关闭。 timer
会一直运行。
Timer
定时器,和 Tick
类似,经过 duration
时间, Timer
会触发,并且往 channel
写入当前时间点,此时 Timer
不再计时。当应用层重新调用 Reset
函数,才又开始计时,这个是和 Tick
不同的。 Tick
是周期性的计时。
Timer
还支持计时结束时,触发自定义函数。 AfterFunc
会返回 Timer
实例。 AfterFunc
调用后,只会计时结束后触发一次自定义函数调用,如果需要再次触发,需要显示调用 Timer.Reset
函数。
如果结束定时器,调用 Stop
即可。
Ticker 运行机制
在 go 源码的 time/tick.go
中,可以看到 NewTicker
实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func NewTicker(d Duration) *Ticker {
if d <= 0 {
panic(errors.New("non-positive interval for NewTicker"))
}
// Give the channel a 1-element time buffer.
// If the client falls behind while reading, we drop ticks
// on the floor until the client catches up.
c := make(chan Time, 1)
t := &Ticker{
C: c,
r: runtimeTimer{
when: when(d),
period: int64(d),
f: sendTime,
arg: c,
},
}
startTimer(&t.r)
return t
}
可以看到 channel
是带有1个元素的缓冲区。重点关注 runtimeTimer
的定义
when
何时触发定时器。所有的时间值都是纳秒级别,int64
表示。period
: 触发周期。根据此值计算下一次触发时间点, 可以简单理解成when = when + period
。当前时间触发后,会更新when
值。f
: 定时器触发时,调用的函数。注意这个函数必须是非阻塞的,否则会阻塞整个timer
的执行。arg
: 调用函数f
时,传入的参数值。
看到 Ticker
对应的触发函数是 sendTime
, 函数实现如下(time/sleep.go
):
1
2
3
4
5
6
7
8
9
10
11
func sendTime(c interface{}, seq uintptr) {
// Non-blocking send of time on c.
// Used in NewTimer, it cannot block anyway (buffer).
// Used in NewTicker, dropping sends on the floor is
// the desired behavior when the reader gets behind,
// because the sends are periodic.
select {
case c.(chan Time) <- Now():
default:
}
}
会把当前的时间点放入到 channel
中,如果应用层没有及时获取 channel
中的值,会直接丢弃当前的时间点,走 default
逻辑。
startTimer
是开启了定时器,此时定时器启动执行。 startTimer
相应代码在 runtime/time.go
中。startTimer -> addtimer -> addtimerLocked
, 具体实现在 addtimerLocked
中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
func addtimerLocked(t *timer) {
// when must never be negative; otherwise timerproc will overflow
// during its delta calculation and never expire other runtime timers
if t.when < 0 {
t.when = 1 << 63 - 1
}
t.i = len(timers.t)
timers.t = append(timers.t, t)
siftupTimer(t.i)
if t.i == 0 {
// siftup moved to top: new earliest deadline
if timers.sleeping {
timers.sleeping = false
notewakeup(&timers.waitnote)
}
if timers.rescheduling {
timers.rescheduling = false
goready(timers.gp, 0)
}
}
if !timers.created {
timers.created = true
go timerproc()
}
}
timers
是全局变量,管理所有的 timer
, 使用数组维护的。数组中的 timer
是有序的,使用了堆排序算法,把 when
最小值排到数组前面,也就是把最先触发定时的 timer
排在最前。
每次新增定时器 timer
时,会调用 siftupTimer
调整数组顺序。
如果当前 timers
是空数组,需要调整 timers
状态, sleeping =false, rescheduling =false
, 如果 timerproc
的 goroutine 为 idle
状态,进行唤醒。调用 goready
函数。
如果 timers
是初次创建,会调用 timerproc
,定时器逻辑全在这里实现。
timerproc
死循环执行, 执行逻辑如下
- 获取当前时间点,纳秒级别
- 如果
timers
数组为空,把执行timerproc
的goroutine
置为idle
状态,节省资源,不必要的空转 - 如果
timers
非空,取出timers
数组中的第一个值,与now
比较timer
的when
值, 如果when
值大,说明timers
中的所有定时器都还未触发,timerproc
的goroutine sleep
, 直到when
值时刻 - 如果数组第一值的
when < now
, 说明已经到了触发时间点,如果timer
的period
有值,说明是周期性触发,更新timer
下次触发时间点
1
2
3
delta = t.when - now
t.when += t.period * (1 + -delta/t.period)
siftdownTimer(0)
下次的时间点,不是简单的 t.when += t.period
, 而是在 0 ~ period
之间。考虑调度因素,选择 0 ~ period
更合理,保证在 period
内会触发。siftdownTimer
使用堆排序算法调整 timer
顺序, timer
的 when
值增加了,可能需要下沉,排在数组后面。
- 如果没有设置
period
值,则移除timers
数组 - 调用
timer.f
函数,当然,timer.arg
是其中的参数
1
2
3
4
f := t.f
arg := t.arg
seq := t.seq
f(arg, seq)
Ticker.Stop
函数最终实现对应 runtime/time.go
中的 deltimer
函数。实际就是把 timer
从 timers
数组中删除,删除之后,还需要调整 timers
的数组顺序。
Timer 的运行机制
Timer
和 Ticker
底层实现走的是同一样一套逻辑。 NewTimer
定义在 runtime/sleep.go
中实现
1
2
3
4
5
6
7
8
9
10
11
12
13
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
}
和 Tick
定义类似,但缺少了 period
的定义。如果定时器触发了, Timer
会被 timers
移除,不在 timers
数组中运行。如果需要运行,需要再次调用 Reset
函数。这里可以看到,触发的函数也是 sendTime
, 和 Tick
是一样的。
1
2
3
4
5
6
7
8
9
10
func (t *Timer) Reset(d Duration) bool {
if t.r.f == nil {
panic("time: Reset called on uninitialized Timer")
}
w := when(d)
active := stopTimer(&t.r)
t.r.when = w
startTimer(&t.r)
return active
}
Reset
主要是重新计算了 when
值,加入到了 timers
数组中,等待再次触发。
Timer
还支持自定义的函数处理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func AfterFunc(d Duration, f func()) *Timer {
t := &Timer{
r: runtimeTimer{
when: when(d),
f: goFunc,
arg: f,
},
}
startTimer(&t.r)
return t
}
func goFunc(arg interface{}, seq uintptr) {
go arg.(func())()
}
如果使用 AfterFunc
, 定时器触发时会调用 goFunc
函数,参数 arg
就是我们在 AfterFunc
中自定义的函数 f
。 AfterFunc
返回 Timer
实例,这样可以显示调用 Stop
进行关闭定时器,或者调用 Reset
也能再次触发。
Ticker 和 Timer 的区别
- 如果是周期性的调用,推荐使用
Ticker
, 性能更高,实现更简单 - 如果只是偶尔的触发定时器,使用
Timer
,更节省资源,就算不调用Stop
,触发一次后,也不会一直存在在timers
数组中 Timer
使用更灵活些,支持自定义函数的场景Ticker
需要关注资源泄露的情况,如果Ticker
不在使用,要显示调用Stop
,否则会一直存在在timers
数组中
Sleep 实现机制
sleep
底层实现在 runtime/time.go
函数中,对应函数为 timeSleep
。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func timeSleep(ns int64) {
if ns <= 0 {
return
}
t := getg().timer
if t == nil {
t = new(timer)
getg().timer = t
}
*t = timer{}
t.when = nanotime() + ns
t.f = goroutineReady
t.arg = getg()
lock(&timers.lock)
addtimerLocked(t)
goparkunlock(&timers.lock, "sleep", traceEvGoSleep, 2)
}
也是开启了一个计时器,计算了触发时间值 when
, 对应的触发函数 goroutineReady
, 触发时会把 goroutine
的等待状态变为运行状态。 goparkunlock
会把当前的 goroutine
变为等待状态。
缺少 period
的定义,这样也是触发一次,会在 timers
数组中移除。
总结
- golang timer 在底层实现上,支持纳秒级别
- 各个 timer 不是单独进行系统调用获取时间,而是
timers
统一调用,性能更高 - 触发时间点尽力保证在
period
内,如果period
比较小,在高CPU
压力下,也很难保证。这种情况下,看到的现象是,有时会隔一段时间(比period
长)触发,但是下次触发非常快,因为这时,下次的when
值不会更新 - 已经存在
timer
的情况下,调低period
或者duration
值,性能影响比较小 - 如果会产生大量
timer
的情况下,性能比较差,数组元素多,每次都要进行堆排序算法调整 - Timer 的启动或者
Reset
会计算when
值,这时会获取系统函数,大量使用Timer
性能会比较差 - Ticker 要注意资源泄露,不使用的情况需要及时
Stop
,否则会一直存在在数组中