命令记不住

登陆

定时器 Timer 和 Ticker

定时器 Timer 和 Ticker

一、1 次性定时器 Timer

1.Timer 简介

Timer 经过指定的时间后触发一个事件,这个事件通过其本身提供的 channel 进行通知。之所以叫单一事件,是因为 Timer 只执行一次就结束。

通过 timer.NewTimer(d Duration)可以创建一个 Timer,参数既等待的时间,时间到来后立即触发一个事件。

Timer 的数据结构

type    Timer struct {
    C <- chan Time
    r    runtimeTimer
}

Timer 对外仅暴露一个 channel,指定的时间到来时就该往 channel 中写入系统时间,既一个事件

2.使用场景

(1)设定超时时间

从一个连接中等待数据

func WaitChannel (conn <- chan string) bool{
    timer := time.NewTimer(1 * time.Second)

    select {
    case <- conn:
        timer.Stop()
        return true
      
        case <- timer.C:
            println("WaitChannel  timeout!")
            return false
    }
}

WaitChannel 的作用就是检测指定的管道中是否有数据到来,通过 select 语句轮询 conn 和 timer.C 两个管道,timer 会在 1s 后向 timer.C 写入数据,如果 1s 内 conn 还没有数据,则判断为超时。

(2)延迟执行某个方法

希望某个方法在今后的某个时间刻执行

func DelayFunction(){
    timer := time.NewTimer(5 * time.Second)

    select {
    case <- timer.C:
        log.Panicln("Delayed 5s, start to do something.")
    }
}

DelayFunction()会一直等待 timer 的事件到来后才会执行后面的方法

3.Timer 对外接口

(1)创建定时器

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

实际撒谎那个,创建 Timer 意味着把一个计时人物交给系统守护协程,该协程管理着所有的 Timer,当 Timer 的时间到达后向 Timer 的管道中发送当前的时间作为事件。

(2)重置定时器

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

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

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

需要注意的是,重置定时器虽然可以用于修改还未超时的定时器,但正确的使用方式还是针对已过期的定时器或已被停止的定时器,同时其返回值也不可靠,返回值存在的价值仅仅是与前面的版本兼容

实际上,重置定时器意味着通知系统守护协程移除该定时器,重新设置时间后,再把定时器交给守护协程

4.简单接口

除了标准接口,time 包同时还提供了一些简单的方法,在特定的场景下可以简化代码

(1)After()

有时候我们就是想等待指定的时间,没有提前停止定时器的需求,也没有复用该定时器的需求,那么可以使用匿名的定时器

使用 func After(d Duration) <- chan Time 方法创建一个定时器,并返回定时器的管道

func AfterDemo() {
    log.Println(time.Now())
    <-time.After(1 * time.Second)
    log.Println(time.Now())
}

两条打印的时间间隔为 1s,实际上还是一个定时器,但代码变得更加简洁

(2)AfterFunc()

前面的例子中讲到延迟一个方法的调用,实际上通过 AfterFunc 可以更简洁,而且可以自定义执行的方法,AfterFunc 的原型为:

func AfterFunc (d Duration, f func()) *Timer

该方法在指定时间到来后会执行函数 f.例如:

func AfterFuncDemo(){
    log.Println("AfterFuncDemo start: ", time.Now())
    time.AfterFunc(1 * time.Second, func() {
        log.Println("AfterFuncDemo end :",time.Now())
    })
    time.Sleep(2 * time.Second) //等待协程退出
}

AfterFuncDemo()中先打印了一个时间,然后使用 AfterFunc 启动一个定时器,并指定定时器结束时执行一个方法打印结束时间

与上面例子不同的是,time.AfterFunc()是异步执行的,所以需要函数最后“sleep”等待指定的协程退出,否则可能函数结束时协程还未执行

5.小结

  • time.NewTimer(d):创建一个 Timer
  • timer.Stop():停止当前 Timer
  • time.Reset(d):重置当前 Timer

二、周期性定时器 Ticker

1.Ticker 简介

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

Ticker 的数据结构与 Timer 非常类似:

type Ticker struct{
    c <-chan Time
    r   runtimeTimer
}

Ticker 对外仅暴露一个 channel,指定的时间到来时就往该 channel 中写入系统时间,既一个事件

在创建 Ticker 时会指定一个时间,作为事件触发的周期,这也是 Ticker 与 Timer 最主要的区别,另外,Ticker 的英文原意是钟表的”滴答“声,钟表周期性地产生”滴答“声,既周期性地产生事件

2.使用场景

(1)简单定时任务

有时候希望定时的执行一个任务,这时就可以使用 Ticker 来实现

每隔 1s 记录一次日志:

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

    for range ticker.C {
        log.Println("Ticker tick.")
    }
}

上述代码中,for range ticker.C 会持续从管道中获取事件,收到事件后打印一行日志,如果管道中没有数据则会阻塞等待事件,由于 Ticker 会周期性地向管道中写入事件,所以上述程序会周期性地打印日志

(2)定时聚合任务

有时我们希望把一些任务打包进行批量处理,比如,公交车发车场景:

  • 公交车每隔 5 分钟发一班,不管是否已坐满乘客
  • 已坐满乘客情况下,不足 5 分钟也发车
//用于演示聚合任务用法

func TickerLaunch(){
    ticker := time.NewTicker(5 * time.Minute)
    maxPassenger := 30          //最大乘客
    passengers := make([]string,0,maxPassenger)
  
    for {
        passenger := GetNewPassenger() //获取一个新乘客
        if passenger != ""{
            passengers = append(passengers,passenger)
        }else {
            time.Sleep(1 * time.Second)
        }

        select {
        case <- ticker.C:   //时间到,发车
        Launch(passengers)
        passengers = []string{}
        default:
            if len(passengers) >= maxPassenger{
                //时间没到,车已经坐满,发车
                Launch(passengers)
                passengers = []string{}
            }
        }
    }
}

上面的代码中 for 循环负责接待乘客上车,并决定是否要发车。每当有乘客上车,select 语句会先判断 ticker.C 中是否有数据,有数据则代表发车事件已到,如果没有数据,则判断车是否已坐满,坐满后仍然发车

3.Ticker 对外接口

(1)创建定时器

使用 NewTicker 方法就可以创建一个周期性定时器,函数原型如下:

func NewTicker (d Duration) *Ticker

其中参数 d 为定时器事件触发的周期

(2)停止定时器

使用定时器对外暴露的 Stop 方法就可以停止一个周期性定时器,函数原型如下:

func (t *Ticker) Stop()

需要注意的是,该方法会停止计时,意味着不会向定时器的管道中写入事件,但管道并不会被关闭。管道在使用完,生命周期结束后会自动释放。

Ticker 在使用完后务必要释放,否者会产生资源泄露,进而会持续消耗 CPU 资源,最后会把 CPU 资源耗尽。

4.简单接口

在有些场景下,我么你启动一个定时器后该定时器永远不会停止,比如定时轮询任务,此时可以使用一个简单的 Tick 函数来获取定时器的管道,函数原型如下:

func Tick(d Duration) <- chan Time

这个函数内部实际上还是创建了一个 Ticker,但并不会返回,所以没有手段来停止该 Ticker,所以,一定要考虑具体的使用场景

5.错误示例

Ticker 用于 for 循环时,很容易出现意想不到的资源泄露问题

资源泄露:

func WrongTicker(){
    for {
        select {
        case <- time.Tick(1 * time.Second):
            log.Printf("Resource leak!")
        }
    }
}

上面的代码中,select 每次检测 case 语句时都会创建一个定时器,for 循环又会不断地执行 select 语句,所以系统里会有越来越多的定时器不断地消耗 CPU 资源,最终 CPU 资源会被耗尽

6.小结

  • 使用 time.NewTicker()创建一个定时器
  • 使用 Stop()停止一个定时器
  • 定时器使用完毕要释放,否则会产生资源泄露
推荐阅读