第一次看到 time.After 出现在 select 里时,我搞不明白——它凭什么能和 channel 一起被监听?
timer大家应该都在学习goroutine和channel时了解过,一个比较经典的题目就是开启goroutine获取结果,main监听结果+超时退出。
go func() {
time.Sleep(time.Second * 2)
result <- 1
}()
select {
case <-result:
log.Println("done")
case <-time.After(time.Second * 3):
log.Println("timeout")
}
close(result)当时我对time.After这个函数很疑惑,为什么这个函数能和result通道一样被select监听。
之后我了解到了timer这个数据类型,类似于一个定时器,到时间自动返回信号。
timer := time.NewTimer(time.Second * 3)
defer timer.Stop()
select {
case <-timer.C:
log.Println("3s到了")
case <-ctx.Done():
log.Println("请求取消/超时")
}golang的timer中最重要的是它的channel字段,因为channel由接收定时的信号。实际上Newtimer这个方法在go runtime创建了一个底层的timer,创建了一个定时事件,在参数时间后向用户态的timer.C(channel字段)发送一个信号。
用户态用select监听channel的信号,监听到信号后协程停止阻塞并被唤醒,执行case中的逻辑。
回到题目中,time.After这个函数似乎也很好理解了,大概就是返回了一个3s的timer.C被当前协程监听。
func After(d Duration) <-chan Time {
return NewTimer(d).C
}源码部分和猜想的一样,只是返回了一个timer.C。
timer还有一个需要注意的重点是timer.Stop方法。不知道大家有没有注意到,在Newtimer后通常会跟一个defer timer.Stop()。
timer := time.NewTimer(time.Second * 3)
defer timer.Stop()
select {
case <-timer.C:
log.Println("3s到了")
case <-ctx.Done():
log.Println("请求取消/超时")
}上面的代码我们可以看出来,如果还没有到3s,但ctx被cancel了,这个函数就退出了。那为什么要Stop呢?主要原因是,每次Newtimer都会在底层创建一个定时事件,定时事件实际是分配在堆上的。虽然函数退出了,创建的底层事件runtime timer不会立刻消失。到达触发时间时,底层timer会向用户态timer.C发送信号,但没有人接收了。
如果是高并发场景下呢?比如我的选课项目,每秒1万请求,每个请求创建1个timer,但是大部分1s就结束了,如果不Stop timer,runtime会积压大量无意义的定时事件。最终会导致堆内存上涨,GC压力增加。
养成defer timer.Stop()的习惯,是一个很小但是值得注意的细节。


