Golang - 定时器原理及使用
一、引入
在进行并发编程时,有时候会需要定时功能,比如监控某个GO程是否会运行过长时间、定时打印日志等等。
GO标准库中的定时器主要有两种:Timer一次性定时器、Ticker周期性定时器。
Timer计时器使用一次后,就失效了,需要Reset()才能再次生效。而Ticker计时器会一直生效。
二、Timer定时器(一次性)
1)实现原理
在一个GO进程中,其中的所有计时器都是由一个运行着 timerproc() 函数的 goroutine 来保护。它使用时间堆(最小堆)的算法来保护所有的 Timer,其底层的数据结构基于数组的最小堆,堆顶的元素是间隔超时最近的 Timer,这个 goroutine 会定期 wake up,读取堆顶的 Timer,执行对应的 f 函数或者 sendtime()函数(下文会对这两个函数进行介绍),而后将其从堆顶移除。
Timer的结构:【不管哪种计时器,.C都是一个chan Time类型且容量为1的单向Channel】
type Timer struct {
C <-chan Time //用于接收 Timer 所触发的事件,当计时器的消息事件(例如:到期)发生时,该 channel 会接收到通知
// contains filtered or unexported fields
}
Timer中对外暴露的只有一个channel,这个 channel 也是定时器的核心。当计时结束时,Timer会发送值到channel中,外部环境在这个 channel 收到值的时候,就代表计时器超时了,可与select搭配执行一些超时逻辑。可以通过time.NewTimer、time.AfterFunc或者 time.Afte对一个Timer进行创建。
2)time.NewTimer()
简单的使用如下:
timer := time.NewTimer(1 * time.Second) // 1s后触发
<-time.C // 这里会阻塞,等待定时器触发
Stop()停止 Timer
func (t *Timer) Stop() bool
Stop()是 Timer 的一个方法,调用Stop()方法,会停止这个 Timer 的计时,使其失效,之后触发定时事件。
实际上,调用此方法后,此Timer会被从时间堆中移除。
Reset()重置Timer
注意,Timer定时器超时一次后就不会再次运行,所以需要调用Reset函数进行重置。
重置定时器虽然可以用于修改还未超时的定时器,但正确的使用方式还是针对于已过期或已被停止的定时器,同时其返回值也不可靠,返回值存在的价值仅仅是与前面版本兼容。
实际上,重置定时器意味着通知系统守护协程移除该定时器,重新设定时间后,再把定时器交给协程。
修改select中代码,在Case中添加一个重置的代码:
select {
case <-h.t.C:
fmt.Println("timer")
h.t.Reset(1*time.Second)
}
可以看到,会不停的打印timer,这是因为使用了Reset函数重置定时器。
注意:不能随意的对Reset方法进行调用,官网文档中特意强调:
For a Timer created with NewTimer, Reset should be invoked only on stopped or expired timers with drained channels.
//对于使用 NewTimer 创建的计时器,仅应在通道已耗尽的停止或过期计时器上调用 Reset。
除非Timer已经被停止或者超时了,否则不要调用Reset方法,原因:如果这个 Timer 还没超时,不先去Stop它,而是直接Reset,那么旧的 Timer 仍然存在,并且仍可能会触发,会产生一些意料之外的事。
所以通常使用如下的代码,安全的重置一个不知状态的Timer(以上的代码中,Reset调用时,总是处于超时状态):
if !t.Stop() {
select {
case <-h.t.C:
default:
}
}
h.t.Reset(1*time.Second)
3)time.After()
匿名定时器,此方法就像是一个极简版的Timer使用,调用time.After(),会直接返回一个channel,当超时后,此channel会接受到一个值,触发后定时器自动停止并销毁。
简单使用如下:
package main
import (
"fmt"
"time"
)
func main() {
fmt.Println("main")
go ticker()
time.Sleep(100 * time.Second)
}
func ticker() {
for {
select {
case <-time.After(1 * time.Second):
fmt.Println("timer")
}
}
}
注意,此方法虽然简单,但是没有Reset方法来重置定时器,但可以搭配for 和select的重复调用来模拟重置。
4)sendtime函数
NewTimer和After这两种创建方法,会Timer在超时后,执行一个标准库中内置的函数:sendTime,来将当前的时间发送到channel中。
5)time.AfterFunc
此方法可以接受一个func类型参数,在计时结束后,会运行此函数,查看以下代码,猜猜会出现什么结果?
package main
import (
"fmt"
"time"
)
func main() {
fmt.Println("main")
t := time.AfterFunc(1*time.Second, func() {
fmt.Println("timer")
})
go timer(t)
time.Sleep(10 * time.Second)
}
func timer(t *time.Timer) {
select {
case <-t.C:
fmt.Println("123")
}
}
结果只打印了main以及timer。这是因为此方法并不会调用上文提到的sendtime()函数,即不会发送值给Timer的Channel,所以select就会一直阻塞。
6)f函数
特意将AfterFunc和以上的NewTimer和After,就是因为f函数的存在。这种方式创建的Timer,在到达超时时间后会在单独的goroutine里执行函数f,而不会执行sendtime函数。
注意,外部传入的f参数并非直接运行在timerproc中,而是启动了一个新的goroutine去执行此方法。
三、Ticker定时器(周期性)
Ticker定时器可以周期性地不断地触发时间事件,不需要额外的Reset操作,其使用方法与Timer大同小异。
需要注意的是每一个 NewTicker 方法开启的计时器都要在不需要使用时调用 Stop 进行关闭,如果不显示调用 Stop 方法,创建的计时器就没有办法被垃圾回收,而通过 Tick 创建的计时器由于只对外提供了 Channel,所以是一定没有办法关闭的,一定要谨慎使用这一接口创建计时器。
通过time.NewTicker对Ticker进行创建,简单的使用如下:
package main
import (
"fmt"
"time"
)
func main() {
fmt.Println("main")
t:=time.NewTicker(1*time.Second)
go timer(t)
time.Sleep(10 * time.Second)
}
func timer(t *time.Ticker) {
for{
select {
case <-t.C:
fmt.Println("timer")
}
}
}
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 如何使用 Uni-app 实现视频聊天(源码,支持安卓、iOS)
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)