当前位置:   article > 正文

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

go 计时

前言

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

计时器主要结构

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

  1. type runtimeTimer struct {
  2. pp uintptr
  3. when int64
  4. period int64
  5. f func(interface{}, uintptr) // NOTE: must not be closure
  6. arg interface{}
  7. seq uintptr
  8. nextwhen int64
  9. status uint32
  10. }
  • p p—地址

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

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

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

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

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

  • status — 计时器的状态;

Timer计时器

Timer结构体

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

Timer 方法

time.NewTimer

实例

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

源码

  1. // NewTimer创建一个新计时器,该计时器将在时间段d后在其通道上发送当前时间。
  2. func NewTimer(d Duration) *Timer {
  3. c := make(chan Time, 1)
  4. t := &Timer{
  5. C: c,
  6. r: runtimeTimer{
  7. when: when(d),
  8. f: sendTime,
  9. arg: c,
  10. },
  11. }
  12. startTimer(&t.r)
  13. return t
  14. }
  15. func sendTime(c interface{}, seq uintptr) {
  16. select {
  17. case c.(chan Time) <- Now():
  18. default:
  19. }
  20. }

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

time.After

实例

  1. //⭐️这个例子有问题
  2. mychan := make(chan int)
  3. go func() {
  4. fmt.Println("wait 4's")
  5. time.Sleep(time.Second * 4)
  6. mychan <- 1
  7. }()
  8. for{
  9. select {
  10. case <-mychan:
  11. fmt.Println("结束了")
  12. // 拿掉return, 就会出现问题
  13. return
  14. case <-time.After(time.Second * 1):
  15. fmt.Println("wait 1's ")
  16. }
  17. }

源码

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

内存泄漏问题分析

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

time.AfterFunc

实例

  1. mychan := make(chan int)
  2. time.AfterFunc(6*time.Second, func() {
  3. fmt.Println("you must wait 6's ")
  4. mychan <- 1
  5. })
  6. for {
  7. select {
  8. case <-mychan:
  9. fmt.Println("Game Over")
  10. return
  11. default:
  12. fmt.Println("wait 3's")
  13. time.Sleep(3 * time.Second)
  14. }
  15. }

源码

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

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

time.stop

源码

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

time.Reset

源码

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

time.Reset存在的问题

正常的情况

  1. c := make(chan bool)
  2. go func() {
  3. // 生产
  4. for i := 0; i < 5; i++ {
  5. time.Sleep(time.Second * 1)
  6. c <- false
  7. }
  8. time.Sleep(time.Second * 1)
  9. c <- true
  10. }()
  11. go func() {
  12. // 消费
  13. timer := time.NewTimer(time.Second * 5)
  14. for {
  15. // 如果过期了将,chan消费完
  16. if !timer.Stop() {
  17. <-timer.C
  18. }
  19. timer.Reset(time.Second * 5)
  20. select {
  21. // 没有过期,判断chan里的数据
  22. case b := <-c:
  23. if b == false {
  24. fmt.Println(time.Now(), ":recv false. continue")
  25. continue
  26. }
  27. // 如果为true就停止吧
  28. fmt.Println(time.Now(), ":recv true. return")
  29. return
  30. //过期了
  31. case <-timer.C:
  32. fmt.Println(time.Now(), ":timer expired")
  33. continue
  34. }
  35. }
  36. }()
  37. // 阻塞用
  38. var s string
  39. fmt.Scanln(&s)

返回结果 

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

错误情况

  1. c := make(chan bool)
  2. go func() {
  3. // 生产
  4. for i := 0; i < 5; i++ {
  5. time.Sleep(time.Second * 6)
  6. c <- false
  7. }
  8. time.Sleep(time.Second * 1)
  9. c <- true
  10. }()
  11. go func() {
  12. // 消费
  13. timer := time.NewTimer(time.Second * 5)
  14. for {
  15. // 如果过期了将,chan消费完
  16. if !timer.Stop() {
  17. <-timer.C
  18. }
  19. timer.Reset(time.Second * 5)
  20. select {
  21. // 没有过期,判断chan里的数据
  22. case b := <-c:
  23. if b == false {
  24. fmt.Println(time.Now(), ":recv false. continue")
  25. continue
  26. }
  27. // 如果为true就停止吧
  28. fmt.Println(time.Now(), ":recv true. return")
  29. return
  30. //过期了
  31. case <-timer.C:
  32. fmt.Println(time.Now(), ":timer expired")
  33. continue
  34. }
  35. }
  36. }()
  37. // 阻塞用
  38. var s string
  39. fmt.Scanln(&s)

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

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

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

解决方法

  1. c := make(chan bool)
  2. go func() {
  3. // 生产
  4. for i := 0; i < 5; i++ {
  5. time.Sleep(time.Second * 6)
  6. c <- false
  7. }
  8. time.Sleep(time.Second * 1)
  9. c <- true
  10. }()
  11. go func() {
  12. // 消费
  13. timer := time.NewTimer(time.Second * 5)
  14. for {
  15. // 如果stop失败进去
  16. if !timer.Stop() {
  17. // <-timer.C失败直接走默认---往下接着走
  18. select {
  19. case <-timer.C:
  20. default:
  21. }
  22. }
  23. timer.Reset(time.Second * 5)
  24. select {
  25. case b := <-c:
  26. // false打印接着跑
  27. if b == false {
  28. fmt.Println(time.Now(), ":recv false. continue")
  29. continue
  30. }
  31. // 返回true就停止
  32. fmt.Println(time.Now(), ":recv true. return")
  33. return
  34. case <-timer.C:
  35. fmt.Println(time.Now(), ":timer expired")
  36. continue
  37. }
  38. }
  39. }()
  40. // 阻塞用
  41. var s string
  42. fmt.Scanln(&s)

Ticker

time.Tick

源码

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

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

time.NewTicker

源码

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

stop

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

实例

  1. ticker := time.NewTicker(time.Second * 1)
  2. go func() {
  3. for t := range ticker.C {
  4. fmt.Println("i am come at ", t)
  5. }
  6. }()
  7. time.Sleep(time.Second * 5)
  8. 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

 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Gausst松鼠会/article/detail/314370
推荐阅读
  

闽ICP备14008679号