一起学Go之计时器(Timer/Tick)

前言

上篇文章写了关于context 源码解读,里面涉及到不少的计时器,所以我们这篇文章就简单了解下go的计时器。Go的计时器主要TimerTicker两种,下面我们开始一起学习下

计时器主要结构

go的计时器基于Go运行时计时器runtime.timer实现的,rumtime.timer的结构体表示如下

type runtimeTimer struct {
   pp       uintptr
   when     int64
   period   int64
   f        func(interface{}, uintptr) // NOTE: must not be closure
   arg      interface{}
   seq      uintptr
   nextwhen int64
   status   uint32
}
  • p p—地址

  • when — 当前计时器被唤醒的时间;

  • period — 两次被唤醒的间隔;

  • f — 每当计时器被唤醒时都会调用的函数;

  • arg — 计时器被唤醒时调用 f 传入的参数;

  • nextWhen — 计时器处于 timerModifiedLater/timerModifiedEairlier 状态时,用于设置 when 字段;

  • status — 计时器的状态;

Timer计时器

Timer结构体

type Timer struct {
    C <-chan Time // 单向chan,将时间写入chan
    r runtimeTimer
}

Timer 方法

time.NewTimer

实例

t := time.NewTimer(time.Second * 2)
defer t.Stop()
for {
   <-t.C
   fmt.Println("timer running...")
   // 重置Reset 使 t 重新开始计时
   t.Reset(time.Second * 2)

}

源码

// NewTimer创建一个新计时器,该计时器将在时间段d后在其通道上发送当前时间。
func NewTimer(d Duration) *Timer {
   c := make(chan Time, 1)
   t := &Timer{
      C: c,
      r: runtimeTimer{
         when: when(d),
         f:    sendTime,
         arg:  c,
      },
   }
   startTimer(&t.r)
   return t
}

func sendTime(c interface{}, seq uintptr) {
    select {
    case c.(chan Time) <- Now():
    default:
    }
}

sendTime将当前时间发送到Timer的时间channel中(NewTimer创建的一个带缓冲的channel)。Timer.C这个channel有没有接收方sendTime都可以非阻塞的将当前时间发送给Timer.C,而且sendTime中还加了双保险:通过select判断Timer.CBuffer是否已满,一旦满了会直接退出,不会阻塞。

time.After

实例

//⭐️这个例子有问题
mychan := make(chan int)

go func() {
   fmt.Println("wait 4's")
   time.Sleep(time.Second * 4)
   mychan <- 1
}()
for{
   select {
   case <-mychan:
      fmt.Println("结束了")
      // 拿掉return, 就会出现问题
       return
   case <-time.After(time.Second * 1):
      fmt.Println("wait 1's ")
   }
}

源码

//在等待持续时间过后,然后在返回的通道上发送当前时间。
//它相当于NewTimer方法。在计时器触发之前,垃圾收集器不会恢复基础计时器。
//如果效率是一个问题,请改用新定时器并调用定时器。如果不再需要定时器,请停止。
func After(d Duration) <-chan Time {
   return NewTimer(d).C
}

内存泄漏问题分析

首先我们从源码看出来,after是创建新的Timer对象的,然后我们如果把return拿掉,那么这个代码后续会一直走after,不停的创建新的对象。所以我们以后如果在for-select的情况下我们可以考虑使用其他的,如果是单独使用记得加上defer 来停止任务。

time.AfterFunc

实例

mychan := make(chan int)
time.AfterFunc(6*time.Second, func() {
   fmt.Println("you must wait 6's ")
   mychan <- 1
})

for {
   select {
   case <-mychan:
      fmt.Println("Game Over")
      return
   default:
      fmt.Println("wait 3's")
      time.Sleep(3 * time.Second)
   }
}

源码

//AfterFunc等待d 的时间段过去,然后在自己的goroutine中调用函数f。它返回一个计时器,能够用于使用其Stop方法取消调用。
func AfterFunc(d Duration, f func()) *Timer {
   t := &Timer{
      r: runtimeTimer{
         when: when(d),
         f:    goFunc,
         arg:  f,
      },
   }
   startTimer(&t.r)
   return t
}

从上面源码可以看到外面传入的f参数并非直接赋值给了运行时计时器的f,而是作为包装函数goFunc的参数传入的。goFunc会启动了一个新的goroutine来执行外部传入的函数f。这是因为所有计时器的事件函数都是由Go运行时内唯一的goroutine timerproc运行的。为了不阻塞timerproc的执行,必须启动一个新的goroutine执行到期的事件函数。

time.stop

源码

//停止计时器。如果调用停止计时器,则返回true;如果计时器已过期或已停止,则返回false。
// Stop不会关闭通道,以防止通道读取错误。对于使用AfterFunc(d,f)创建的计时器,
// 如果t.Stop返回false,则计时器已过期,并且函数f已在其自己的goroutine中启动;
// Stop不会在返回前等待f完成。如果调用方需要知道f是否已完成,则必须显式地与f协调
func (t *Timer) Stop() bool {
   if t.r.f == nil {
      panic("time: Stop called on uninitialized Timer")
   }
   return stopTimer(&t.r)
}

time.Reset

源码

//Reset会将计时器更改为在d时间段后过期。如果计时器处于active,则返回true;
//如果计时器已过期或已停止,则返回false。我们应该在已停止或过期且chan为空的计时器上调用Reset。
//如果一个程序已经从t.C.接收到一个值,则已知定时器已过期,通道已耗尽,因此可以直接使用t.Reset。
//重置计时器时必须注意不要与当前计时器到期发送时间到t.C的操作产生竞争。如果程序已经从t.C接收到值,
//则计时器是已知的已过期,并且t.Reset可以直接使用。如果程序尚未从t.C接收值,计时器必须先被停止,
//并且-如果使用t.Stop时报告计时器已过期,那么请排空其通道中值
func (t *Timer) Reset(d Duration) bool {
   if t.r.f == nil {
      panic("time: Reset called on uninitialized Timer")
   }
   w := when(d)
   active := stopTimer(&t.r)
   resetTimer(&t.r, w)
   return active
}

time.Reset存在的问题

正常的情况

c := make(chan bool)

go func() {
   // 生产
   for i := 0; i < 5; i++ {
      time.Sleep(time.Second * 1)
      c <- false
   }
   time.Sleep(time.Second * 1)
   c <- true
}()

go func() {
   // 消费
   timer := time.NewTimer(time.Second * 5)
   for {
      // 如果过期了将,chan消费完
      if !timer.Stop() {
         <-timer.C
      }
      timer.Reset(time.Second * 5)
      select {
      // 没有过期,判断chan里的数据
      case b := <-c:
         if b == false {
            fmt.Println(time.Now(), ":recv false. continue")
            continue
         }
         // 如果为true就停止吧
         fmt.Println(time.Now(), ":recv true. return")
         return 
         //过期了
      case <-timer.C:
         fmt.Println(time.Now(), ":timer expired")
         continue
      }
   }
}()
// 阻塞用
var s string
fmt.Scanln(&s)

返回结果 

2021-08-19 13:27:56.968569 +0800 CST m=+1.002051349 :recv false. continue
2021-08-19 13:27:57.968956 +0800 CST m=+2.002434837 :recv false. continue
2021-08-19 13:27:58.971097 +0800 CST m=+3.004571872 :recv false. continue
2021-08-19 13:27:59.97512 +0800 CST m=+4.008591506 :recv false. continue
2021-08-19 13:28:00.975604 +0800 CST m=+5.009071140 :recv false. continue
2021-08-19 13:28:01.97907 +0800 CST m=+6.012533936 :recv true. return

错误情况

c := make(chan bool)

go func() {
   // 生产
   for i := 0; i < 5; i++ {
      time.Sleep(time.Second * 6)
      c <- false
   }
   time.Sleep(time.Second * 1)
   c <- true
}()

go func() {
   // 消费
   timer := time.NewTimer(time.Second * 5)
   for {
      // 如果过期了将,chan消费完
      if !timer.Stop() {
         <-timer.C
      }
      timer.Reset(time.Second * 5)
      select {
      // 没有过期,判断chan里的数据
      case b := <-c:
         if b == false {
            fmt.Println(time.Now(), ":recv false. continue")
            continue
         }
         // 如果为true就停止吧
         fmt.Println(time.Now(), ":recv true. return")
         return
         //过期了
      case <-timer.C:
         fmt.Println(time.Now(), ":timer expired")
         continue
      }
   }
}()
// 阻塞用
var s string
fmt.Scanln(&s)

返回结果(直接阻塞在这里)

2021-08-19 13:30:04.949688 +0800 CST m=+5.006256731 :timer expired

问题原因: 因为生产等了6秒,消费的timer已经过期了,然后在进入到!timer.Stop()(已经过期,在执行stop就为false),在timer.C(chan) 执行 <-,就抛错(阻塞住了) 

解决方法

c := make(chan bool)

go func() {
   // 生产
   for i := 0; i < 5; i++ {
      time.Sleep(time.Second * 6)
      c <- false
   }
   time.Sleep(time.Second * 1)
   c <- true
}()

go func() {
   // 消费
   timer := time.NewTimer(time.Second * 5)
   for {
      // 如果stop失败进去
      if !timer.Stop() {
         // <-timer.C失败直接走默认---往下接着走
         select {
         case <-timer.C:
         default:
         }
      }
      timer.Reset(time.Second * 5)
      select {
      case b := <-c:
         // false打印接着跑
         if b == false {
            fmt.Println(time.Now(), ":recv false. continue")
            continue
         }
         // 返回true就停止
         fmt.Println(time.Now(), ":recv true. return")
         return
      case <-timer.C:
         fmt.Println(time.Now(), ":timer expired")
         continue
      }
   }
}()

// 阻塞用
var s string
fmt.Scanln(&s)

Ticker

time.Tick

源码

//Tick是NewTicker的一个方便使用的包,仅提供对ticking chan 的访问.
// 返回tick的chan,如果d <= 0 则返回 nil
func Tick(d Duration) <-chan Time {
   if d <= 0 {
      return nil
   }
   return NewTicker(d).C
}

注意: time.Tick底层的Ticker不能被垃圾收集器恢复。

time.NewTicker

源码

// NewTicker返回一个新的Ticker,其中包含chan,chan将以d 参数指定的时间段发送时间。
func NewTicker(d Duration) *Ticker {
   if d <= 0 {
      panic(errors.New("non-positive interval for NewTicker"))
   }
   // 为chan提供一个一个元素的时间类型缓冲。
   c := make(chan Time, 1)
   t := &Ticker{
      C: c,
      r: runtimeTimer{
         when:   when(d),
         period: int64(d),
         f:      sendTime,
         arg:    c,
      },
   }
   startTimer(&t.r)
   return t
}

stop

// 关闭 Ticker, 不会关闭chan,以防止因并发读取到错误的 tick 的chan信息
func (t *Ticker) Stop() {
   stopTimer(&t.r)
}

实例

ticker := time.NewTicker(time.Second * 1)
go func() {
   for t := range ticker.C {
      fmt.Println("i am come at ", t)
   }
}()

time.Sleep(time.Second * 5)
ticker.Stop()

tick和timer区别

看了创建方法的时候应该可以知道,Ticker 中的runtimeTimer字段的 period 字段会赋值为 NewTicker(d Duration) 中的d,表示每间隔d纳秒,Timer定时器就周期性地触发时间事件,timer触发一次

推荐阅读

Go 内存泄露之痛,这篇把 Go timer.After 问题根因讲透了!icon-default.png?t=L892https://mp.weixin.qq.com/s/KSBdPkkvonSES9Z9iggElgGo语言计时器的使用详解icon-default.png?t=L892https://mp.weixin.qq.com/s/QahprdKrlcaatG8poWsNrA难以驾驭的 Go timer,一文带你参透计时器的奥秘icon-default.png?t=L892https://mp.weixin.qq.com/s/gxX-q2EvgWZEWe-deRITSw

 

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

木子林_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值