阅读 72

Go 定时器

定时器

Go语言的定时器分为两种:

  • 一次性定时器(Timer):定时器值计时一次,计时结束便停止运行

  • 周期性定时器(Ticker):定时器周期性的进行计时,除非主动停止,否则将永远运行

1.一次性定时器(Timer)

1.1 简介

Timer是一种单一事件的定时器,即经过指定的时间后触发一个事件,这个事件通过其本身提供的channel进行通知。 之所以叫单一事件,是因为Timer只执行一次就结束,这也是一次性定时器与周期性定时器最重要的区别。 通过timer.NewTimer(d Duration)可以创建一个Timer,参数即等待时间,时间到来后立刻触发一个事件

1.2 使用场景

1.2.1 设定超时时间

协程从管道读取数据时,如果管道内没有数据那么协程将被阻塞,一直等待管道中有数据写入;有的时候我们不希望 协程被永久阻塞,而是等待一个指定的时间,如果超过这段时间管道内仍没有数据写入,则协程可以判定为超时, 转而去处理其他逻辑。

例子如下

package main import ( "fmt" "time" ) func WaitChannel(conn <-chan string) bool{ timer:=time.NewTimer(1*time.Second) select{ case <-conn: timer.Stop() return true case <-timer.C: fmt.Println("WaitChannel timeout!") return false } } func main(){ str:=make(chan string) str1:=make(chan string,1) str1<-"abc" ok1:=WaitChannel(str1) fmt.Println(ok1) ok:=WaitChannel(str) fmt.Println(ok) } 复制代码

image-20210727161313060

1.2.2 延迟执行某个方法

有的时候我们希望某个方法在今后某个时刻执行

package main import ( "fmt" "log" "time" ) func DelayFunction(){ timer:=time.NewTimer(5*time.Second) select{ case <-timer.C: log.Println("Delay 5s,Start to do sth.") } } func main() { start:=time.Now() DelayFunction() cost:=time.Since(start) fmt.Println("cost",cost," s") } 复制代码

image-20210727163503985

1.3 Timer对外接口

  1. 创建定时器

    使用func NewTimer(d Duration) *Timer方法指定一个时间即可创建一个Timer,Timer一经创建便开始计时,不需要额外的启动命令。

    创建Timer意味着把一个计时任务交给系统守护协程,该协程管理着所有的Timer,

    当Timer的时间到达后向Timer的管道中发送当前的时间作为事件。

  2. 停止定时器

    Timer创建后可以随时停止,停止计时器方法如下:

    func(t *Timer) Stop() bool

    返回值代表定时器是否超时

    实际上,停止计时器意味着通知系统守护协程移除该定时器

    • true 定时器超时前停止,后续不会再发送事件

    • false 定时器超时后停止

  3. 重置定时器

    已过期的定时器或者已经停止的定时器可以通过重置动作重新激活,重置方法如下:

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

    重置的动作实质上是先停止定时器,再启动,其返回值是停止计时器的返回值。

1.4. 简单接口

除了上面的标准接口,还提供了一些简单方法在特定情况下使用可以减少代码

  1. After()

  2. AfterFunc()

2. 周期性定时器(Ticker)

2.1 简介

Ticker是周期性定时器,即周期性的触发一个事件,通过Ticker本身提供的管道将事件传递出去。

2.2 使用场景

2.2.1 简单定时任务

有时我们希望定时执行一个任务,例如每秒记录一次日志

package main import ( "log" "time" ) func TickerDemo(){ ticker:=time.NewTicker(1*time.Second) defer ticker.Stop() for range ticker.C{ log.Println("Ticker tick.") } } func main(){ TickerDemo() } 复制代码

image-20210728154418662

for range语句会持续性地从管道中获取事件,收到事件后打印一行日志,如果管道中没有数据则会阻塞等待事件。由于Ticker会周期性地向管道写入事件,所以能实现周期性打印

2.2.2 定时聚合任务

有时我们希望把一些任务打包进行批量处理,例如下面的场景:

公交车发车遵循以下规则

  • 公交车每隔5分钟发车,不管是否已经坐满乘客

  • 已经坐满乘客的情况下,不足5分钟也会发车

package main import ( "bytes" "fmt" "math/rand" "time" ) func TickerLaunch(){ ticker:=time.NewTicker(5*time.Minute) maxPassenger:=30 Passengers:=make([]string,0,maxPassenger) for{ passenger:=GetNewPassenger(1) if passenger!=""{ Passengers=append(Passengers,passenger) }else{ time.Sleep(1*time.Second) } fmt.Println(Passengers) select { case<-ticker.C: Passengers=[]string{} default: if len(Passengers)>=maxPassenger{ Passengers=[]string{} } } fmt.Println(Passengers) } } func GetNewPassenger(codeLen int) string{ rawStr :="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890_" buf := make([]byte, 0, codeLen) b := bytes.NewBuffer(buf) rand.Seed(time.Now().UnixNano()) for rawStrLen := len(rawStr);codeLen > 0; codeLen-- { randNum := rand.Intn(rawStrLen) b.WriteByte(rawStr[randNum]) } return b.String() } func main(){ TickerLaunch() } 复制代码

具体看看逻辑就好,死循环不要轻易尝试运行

3. runtimeTimer

上面的两种计时器都会在底层创建一个runtimeTimer,所以每一个版本中runtimeTimer的优化都十分重要

  • Go 1.10之前:所有的runtimeTimer保存在一个全局的堆中;

  • Go 1.10~1.13: runtimeTimer被拆分到多个全局堆中 ,减少了多个系统协程的锁等待时间

  • Go 1.14+ : runtimeTimer保存在每个处理器P中,消除了专门的系统协程,减少了系统协程上下文切换的时间。

4. 注意事项

当我们使用Ticker的时候,如果忘记在使用结束后及时停止Ticker,就会造成资源泄露CPU使用率不断升高的情况

通常,我们在创建Ticker实例的时候就应该接着defer语句将Ticker停止

ticker:=time.NewTicker(1*time.Second) defer ticker.Stop()


作者:shelgi
链接:https://juejin.cn/post/7023566456755322888


文章分类
后端
文章标签
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐