关于golang:Go-Timer-详解以及-Reset-和-Stop-的正确用法

10次阅读

共计 2653 个字符,预计需要花费 7 分钟才能阅读完成。

time.Timer

Timer 是 go 中 time 包里的一种一次性计时器,它的作用是定时触发事件,在触发之后这个 Timer 就会生效,须要调用 Reset() 来让这个 Timer 从新失效。

type Timer struct {
    C <-chan Time
    // contains filtered or unexported fields
}

这个是 Timer 类型的构造体,其中只有一个 channel 可供内部拜访,这个 channel 的作用就是在定时完结完结之后,会发送以后工夫到这个 channel 外面,所以在 channel 收到值的时候,就等于计时器超时了,能够执行定时的事件了。所以个别是和 select 语句搭配应用。

Timer 的底层原理

在一个程序中,其中的所有计时器都是由一个运行着 timerproc() 函数的 goroutine 来保护。它采纳了工夫堆的算法来保护所有的 Timer,其底层的数据结构是基于数组的小根堆,堆顶的元素是间隔超时最近的 Timer,这个 goroutine 会定期 wake up,读取堆顶的 Timer,执行对应的 f 函数或者 send time,而后将其从堆顶移除。

time.NewTimer() 创立 Timer

func NewTimer(d Duration) *Timer

time.NewTimer() 是创立 Timer 的其中一种形式,通过传入一个定时工夫 dtime.NewTimer() 就会返回创立的 Timer 的指针,这个 Timer 会在通过 d 这么长的工夫工夫之后,向 Timer 中的 channel 发送以后工夫。

timer := time.NewTimer(5 * time.Minute)
select {
    case <-timer.C:
       fmt.Println("timed out")
    default:
}

在 Timer 超时之后,select 就会收到 channel 里发送的值,这样就能够往下执行定时事件了。

Stop() 停止 Timer

func (t *Timer) Stop() bool

Stop() 是 Timer 的成员函数,调用 Stop() 办法,会停止这个 Timer 的计时,使其生效,之后是不会触发定时事件的。

调用 Stop() 办法之后,会将这个 Timer 从工夫堆里移除,如果这个 Timer 还没超时,仍然在工夫堆中,那么就会被胜利移除并且返回 true;如果这个 Timer 不在工夫堆里,阐明曾经超时了或者曾经被 stop 了,这个时候就会返回 false

Reset() 重置 Timer

func (t *Timer) Reset(d Duration) bool

Reset() 是 Timer 里的另一个成员函数,它的作用是重置这个 Timer。如果这个 Timer 曾经超时生效了,那么 Reset() 会令其从新失效;如果这个 Timer 还没超时,那么 Reset() 会让其从新计时,并将超时工夫设置为 d

这里有一个须要 留神 的中央,在官网的 package 文档中,有这么一句话:

For a Timer created with NewTimer, Reset should be invoked only on stopped or expired timers with drained channels.

意思是调用 Reset() 之前,肯定要保障这个 Timer 曾经被 stop 了,或者这个 Timer 曾经超时了,并且外面 channel 曾经被排空了。

因为,如果这个 Timer 还没超时,然而不去保障这个 Timer 曾经被 stop 了,那么旧的 Timer 仍然存在工夫堆里,并且仍然会触发,就会产生意料之外的事。而如果这个 Timer 曾经超时了,不在工夫堆里了,然而可能是刚刚超时,并且往 channel 里发送了工夫,如果不显式排空 channel 的话,那么也会触发超时事件,所以须要显式地排空 channel。

所以失常状况下,Reset() 要和 Stop() 一起搭配应用。官网文档里给出了示例:

if !t.Stop() {<-t.C}
t.Reset(d)

这样能够同时保障这个 Timer 曾经被 stop 了,或者这个 Timer 曾经超时了,然而对 channel 进行了显式排空。

然而这里 存在一个问题 ,在失常状况下,如果之前的 Timer 还失效,那么 Stop() 会返回 true,不会产生问题;然而如果 Timer 曾经超时了,Stop() 就会返回 false,而如果 channel 外面没有没有值,那么就会产生 阻塞,导致程序卡在这里。

所以更好的做法是采纳 select

if !t.Stop() {
    select {
    case <-t.C: // try to drain the channel
    default:
    }
}
t.Reset(d)

这样即便 channel 外面没有值,也不会产生阻塞,有值的话也能够胜利排空 channel。

然而,显式排空 channel 并不是相对的,如果 channel 外面存在值,然而对你想要的后果不会产生任何影响的话,那么不显式排空 channel 也是能够的,间接在 Reset() 之前调用一次 Stop() 就行,也不须要对 Stop() 的返回值进行判断。

time.AfterFunc() 创立 Timer

对于 time.Timer,还有另一种创立形式:time.AfterFunc()。(time 包里还有其余几种计时器,这篇文章只探讨 time.Timer 这种计时器)

func AfterFunc(d Duration, f func()) *Timer

time.AfterFunc()time.NewTimer() 相比,参数上多了一个 f。这是因为 time.AfterFunc() 创立的 Timer 在超时之后会在一个新的 goroutine 中执行这个 f 函数,不会向 channel 外面发送值。

之前所探讨的须要 Stop() 之后显式排空 channel 的状况都是对于 time.NewTimer() 创立的 Timer 来说,对于 time.AfterFunc() 来说,因为不会向 channel 里发送值,所以不须要显式排空 channel 的额定操作,然而在 Reset() 之前还是须要调用 Stop() 的。

此外,Stop()Reset() 的返回值对于 time.AfterFunc() 创立的 Timer 来说含意与 之前提到的 是不一样的。须要本人视状况而定,看 f 函数的 执行与否 对后果来说有没有影响,如果有影响,那么就须要额定判断返回值,如果没有影响,间接调用即可。

参考资料

[1] 论 golang Timer Reset 办法应用的正确姿态

[2] package time

[3] How Do They Do It: Timers in Go

[4] Golang Timer 源码摸索及常见问题

正文完
 0