Golang time.Timer and time.Ticker
time.Timer
结构
首先我们看Timer
的结构定义:
type Timer struct {
C <-chan Time
r runtimeTimer
}
其中有一个C
的只读channel
,还有一个runtimeTimer
类型的结构体,再看一下这个结构的具体结构:
type runtimeTimer struct {
tb uintptr
i int
when int64
period int64
f func(interface{}, uintptr) // NOTE: must not be closure
arg interface{}
seq uintptr
}
在使用定时器Timer
的时候都是通过 NewTimer
或 AfterFunc
函数来获取。
先来看一下NewTimer
的实现:
func NewTimer(d Duration) *Timer {
c := make(chan Time, 1)
t := &Timer{
C: c,
r: runtimeTimer{
when: when(d), //表示达到时间段d时候调用f
f: sendTime, // f表示一个函数调用,这里的sendTime表示d时间到达时向Timer.C发送当前的时间
arg: c, // arg表示在调用f的时候把参数arg传递给f,c就是用来接受sendTime发送时间的
},
}
startTimer(&t.r)
return t
}
定时器的具体实现逻辑,都在
runtime
中的time.go
中,它的实现,没有采用经典Unix
间隔定时器setitimer
系统调用,也没有 采用POSIX
间隔式定时器(相关系统调用:timer_create
、timer_settime
和timer_delete
),而是通过四叉树堆(heep
)实现的(runtimeTimer
结构中的i
字段,表示在堆中的索引)。通过构建一个最小堆,保证最快拿到到期了的定时器执行。定时器的执行,在专门的goroutine
中进行的:go timerproc()
。有兴趣的同学,可以阅读runtime/time.go
的源码。
其他方法
func After(d Duration) <-chan Time { return NewTimer(d).C }
根据源码可以看到After
直接是返回了Timer
的channel
,这种就可以做超时处理。
比如我们有这样一个需求:我们写了一个爬虫,爬虫在HTTP GET 一个网页的时候可能因为网络的原因就一只等待着,这时候就需要做超时处理,比如只请求五秒,五秒以后直接丢掉不请求这个网页了,或者重新发起请求。
go Get("http://baidu.com/")
func Get(url string) {
response := make(chan string)
response = http.Request(url)
select {
case html :=<- response:
println(html)
case <-time.After(time.Second * 5):
println("超时处理")
}
}
可以从代码中体现出来,如果五秒到了,网页的请求还没有下来就是执行超时处理,因为Timer
的内部会是帮你在你设置的时间长度后自动向Timer.C
中写入当前时间。
其实也可以写成这样:
func Get(url string) {
response := make(chan string)
response = http.Request(url)
timeOut := time.NewTimer(time.Second * 3)
select {
case html :=<- response:
println(html)
case <-timeOut.C:
println("超时处理")
}
}
-
func (t *Timer) Reset(d Duration) bool
//强制的修改timer
中规定的时间,Reset
会先调用stopTimer
再调用startTimer
,类似于废弃之前的定时器,重新启动一个定时器,Reset
在Timer
还未触发时返回true
;触发了或Stop
了,返回false
。 -
func (t *Timer) Stop() bool
// 如果定时器还未触发,Stop
会将其移除,并返回true
;否则返回false
;后续再对该Timer
调用Stop
,直接返回false
。
比如我写了了一个简单的事例:每两秒给你的女票发送一个"I Love You!"
// 其中协程之间的控制做的不太好,可以使用channel或者golang中的context来控制
package main
import (
"time"
"fmt"
)
func main() {
go Love() // 起一个协程去执行定时任务
stop := 0
for {
fmt.Scan(&stop)
if stop == 1{
break
}
}
}
func Love() {
timer := time.NewTimer(2 * time.Second) // 新建一个Timer
for {
select {
case <-timer.C:
fmt.Println("I Love You!")
timer.Reset(2 * time.Second) // 上一个when执行完毕重新设置
}
}
return
}
-
func AfterFunc(d Duration, f func()) *Timer
// 在时间d后自动执行函数f
func main() {
f := func(){fmt.Println("I Love You!")}
time.AfterFunc(time.Second*2, f)
time.Sleep(time.Second * 4)
}
自动在2秒后打印 "I Love You!"
time.Ticker
如果学会了Timer
那么Ticker
就很简单了,Timer
和Ticker
结构体的结构是一样的,举一反三,其实Ticker
就是一个重复版本的Timer
,它会重复的在时间d后向Ticker
中写数据
-
func NewTicker(d Duration) *Ticker
// 新建一个Ticker -
func (t *Ticker) Stop()
// 停止Ticker -
func Tick(d Duration) <-chan Time
// Ticker.C 的封装
Ticker
和 Timer
类似,区别是:Ticker
中的runtimeTimer
字段的 period
字段会赋值为 NewTicker(d Duration)
中的d
,表示每间隔d
纳秒,定时器就会触发一次。
除非程序终止前定时器一直需要触发,否则,不需要时应该调用 Ticker.Stop
来释放相关资源。
如果程序终止前需要定时器一直触发,可以使用更简单方便的 time.Tick
函数,因为 Ticker
实例隐藏起来了,因此,该函数启动的定时器无法停止。
那么这样我们就可以把发"I Love You!"
的例子写得简单一些。
func main() {
//定义一个ticker
ticker := time.NewTicker(time.Millisecond * 500)
//Ticker触发
go func() {
for t := range ticker.C {
fmt.Println(t)
fmt.Println("I Love You!")
}
}()
time.Sleep(time.Second * 18)
//停止ticker
ticker.Stop()
}
定时器的实际应用
在实际开发中,定时器用的较多的会是 Timer
,如模拟超时,而需要类似 Tiker
的功能时,可以使用实现了 cron spec
的库 cron。
参考
定时器