golang timer定时器
2022-03-19 17:55 youxin 阅读(782) 评论(0) 编辑 收藏 举报Go语言的定时器实质是单向通道,time.Timer结构体类型中有一个time.Time类型的单向chan,源码(src/time/time.go)如下
type Timer struct {
C <-chan Time
r runtimeTimer
初始化 Timer 方法为NewTimer
package main import ( "fmt" "time" ) func main() { t := time.NewTimer(time.Second * 2) defer t.Stop() for { <-t.C fmt.Println("timer running...") // 需要重置Reset 使 t 重新开始计时 t.Reset(time.Second * 2) } }
输出
timer running…
timer running…
timer running…
timer running…
这里使用NewTimer定时器需要t.Reset重置计数时间才能接着执行。如果注释 t.Reset(time.Second * 2)会导致通道堵塞,报fatal error: all goroutines are asleep - deadlock!错误。
同时需要注意 defer t.Stop()在这里并不会停止定时器。这是因为Stop会停止Timer,停止后,Timer不会再被发送,但是Stop不会关闭通道,防止读取通道发生错误。
t := time.NewTimer(time.Second * 2) ch := make(chan bool) go func(t *time.Timer) { defer t.Stop() for { select { case <-t.C: fmt.Println("timer running....") // 需要重置Reset 使 t 重新开始计时 t.Reset(time.Second * 2) case stop := <-ch: if stop { fmt.Println("timer Stop") return } } } }(t) time.Sleep(10 * time.Second) ch <- true close(ch) time.Sleep(1 * time.Second)
定时器(NewTicker)
package main import ( "fmt" "time" ) func main() { t := time.NewTicker(time.Second*2) defer t.Stop() for { <- t.C fmt.Println("Ticker running...") } }
time.After
time.After()表示多长时间长的时候后返回一条time.Time类型的通道消息。但是在取出channel内容之前不阻塞,后续程序可以继续执行。
先看源码(src/time/sleep.go)
func After(d Duration) <-chan Time {
return NewTimer(d).C
}
通过源码我们发现它返回的是一个NewTimer(d).C,其底层是用NewTimer实现的,所以如果考虑到效率低,可以直接自己调用NewTimer。
package main import ( "fmt" "time" ) func main() { t := time.After(time.Second * 3) fmt.Printf("t type=%T\n", t) //阻塞3秒 fmt.Println("t=", <-t) }
基于time.After()特性可以配合select实现计时器
package main import ( "fmt" "time" ) func main() { ch1 := make(chan int, 1) ch1 <- 1 for { select { case e1 := <-ch1: //如果ch1通道成功读取数据,则执行该case处理语句 fmt.Printf("1th case is selected. e1=%v\n", e1) case <-time.After(time.Second*2): fmt.Println("Timed out") } } }
select语句阻塞等待最先返回数据的channel`,如ch1通道成功读取数据,则先输出1th case is selected. e1=1,之后每隔2s输出 Timed out。
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
// 新建一个Tickerfunc (t *Ticker) Stop()
// 停止Tickerfunc 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。
首先time.Timer和 time.NewTicker属于定时器,二者的区别在于
timer : 到固定时间后会执行一次,请注意是一次,而不是多次。但是可以通过reset来实现每隔固定时间段执行
ticker : 每隔固定时间都会触发,多次执行. 具体请查看下面示例1
time.After : 用于实时超时控制,常见主要和select channel结合使用.查看代码示例2
注意点:
没有关闭定时器的执行。定时器未关闭!!!!大家会想到stop ,使用stop注意是在协程内还是携程外,以及使用的场景业务
协程退出时需要关闭,避免资源l浪费,使用defer ticker.Stop()
package main import ( "fmt" "time" ) //定时器的stop func main() { // 协程内的定时器 stop 在协程结束时,关闭默认资源定时器,channel 具体根据业务来看 go func() { ticker := time.NewTicker(5 * time.Second) // 此处 可以简化为defer ticker.Stop() defer func() { fmt.Println("stop") ticker.Stop() }() select { case <- ticker.C: fmt.Println("ticker..." ) } }() // 停止ticker stopChan := make(chan bool) ticker := time.NewTicker(5 * time.Second) go func(ticker *time.Ticker) { defer func() { ticker.Stop() fmt.Println("Ticker2 stop") }() for { select { case s := <-ticker.C: fmt.Println("Ticker2....",s) case stop := <-stopChan: if stop { fmt.Println("Stop") return } } } }(ticker) // 此处的stop 并不会结束上面协程,也不会打印出 Ticker2 stop 只能借助stopChan,让协程结束时关闭ticker或者协程出现panic时执行defer //ticker.Stop() stopChan <- true close(stopChan) time.Sleep(time.Second * 10) fmt.Println("main end") }
timer正确的stop 问题
使用 Golang Timer 的正确方式
https://www.codercto.com/a/34856.html
一、标准 Timer 的问题
以下讨论只针对由 NewTimer 创建的 Timer,因为这种 Timer 会使用 channel 来传递到期事件,而正确操作 channel 并非易事。
Timer.Stop
按照 Timer.Stop 文档 的说法,每次调用 Stop 后需要判断返回值,如果返回 false(表示 Stop 失败,Timer 已经在 Stop 前到期)则需要排掉(drain)channel 中的事件:
if !t.Stop() {
<-t.C
}
但是如果之前程序已经从 channel 中接收过事件,那么上述 <-t.C
就会发生阻塞。可能的解决办法是借助 select 进行 非阻塞 排放(draining):
if !t.Stop() {
select {
case <-t.C: // try to drain the channel
default:
}
}
但是因为 channel 的发送和接收发生在不同的 goroutine,所以 存在竞争条件 (race condition),最终可能导致 channel 中的事件未被排掉。
以下就是一种有问题的场景,按时间先后顺序发生:
select...case <-t.C
Timer.Reset
按照 Timer.Reset 文档 的说法,要正确地 Reset Timer,首先需要正确地 Stop Timer。因此 Reset 的问题跟 Stop 基本相同。
二、使用 Timer 的正确方式
参考 Russ Cox 的回复( 这里 和 这里 ),目前 Timer 唯一合理的使用方式是:
- 程序始终在同一个 goroutine 中进行 Timer 的 Stop、Reset 和 receive/drain channel 操作
- 程序需要维护一个状态变量,用于记录它是否已经从 channel 中接收过事件,进而作为 Stop 中 draining 操作的判断依据
如果每次使用 Timer 都要按照上述方式来处理,无疑是一件很费神的事。为此,我专门写了一个 Go 库 goodtimer 来解决标准 Timer 的问题。懒是一种美德 :-)
本文链接:https://www.codercto.com/a/34856.html
https://www.jianshu.com/p/372f714c2cf3
https://studygolang.com/articles/9289链接:https://www.jianshu.com/p/2b4686b8de4a
http://russellluo.com/2018/09/the-correct-way-to-use-timer-in-golang.html
参考;
https://blog.csdn.net/guyan0319/article/details/90450958