Golang 中 Timer 的陷阱

Golang 的 Timer 类,是一个普遍意义上的定时器,它有着普通定时器的一些特性,例如:

  • 给定一个到期时间,和一个回调函数,到期后会调用回调函数
  • 重置定时器的超时时间
  • 停止定时器

Golang 的 Timer 在源码中,实现的方式是以一个小顶堆来维护所有的 Timer 集合。接着启动一个独立的 goroutine,循环从小顶堆中的检测最近一个到期的 Timer 的到期时间,接着它睡眠到最近一个定时器到期的时间。最后会执行开始时设定的回调函数。Timer 到期之后,会被 Golang 的 runtime 从小项堆中删除,并等待 GC 回收资源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import (
"time"
"fmt"
)


func main() {
timer := time.NewTimer(3 * time.Second)

go func() {
<-timer.C
fmt.Println("Timer has expired.")
}()

timer.Stop()
time.Sleep(60 * time.Second)
}

timer.NewTimer() 会启动一个新的 Timer 实例,并开始计时。 我们启动一个新的 goroutine,来以阻塞的方式从 Timer 的 C 这个 channel 中,等待接收一个值,这个值是到期的时间。并打印”Timer has expired.”

到现在看起来似乎没什么问题,但是当我们执行 timer.Stop() 之后,3 秒钟过去了,程序却没有打印那句话。说明执行 timer.Stop() 之后,Timer 自带的 channel 并没有关闭,而且这个 Timer 已经从 runtime 中删除了,所以这个 Timer 永远不会到期。

这会导致程序逻辑错误,或者更严重的导致 goroutine 和内存泄露。解决的办法是,使用 timer.Reset() 代替 timer.Stop() 来停止定时器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"time"
"fmt"
)


func main() {
timer := time.NewTimer(3 * time.Second)

go func() {
<-timer.C
fmt.Println("Timer has expired.")
}()

//timer.Stop()
timer.Reset(0 * time.Second)
time.Sleep(60 * time.Second)
}

这样做就相当于给 Timer 一个 0 秒的超时时间,让 Timer 立刻过期。