首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >定时器使用避险

定时器使用避险

作者头像
数据小冰
发布2022-08-15 14:40:28
发布2022-08-15 14:40:28
55400
代码可运行
举报
文章被收录于专栏:数据小冰数据小冰
运行总次数:0
代码可运行

定时器实现原理剖析这篇文章小编主要是结合源码分析了定时器是如何实现的。本篇文章,小编将从应用的角度讲述timer使用不当存在的问题。建议读者两篇文章一起看,收益更大。

timer/ticker三要素

我们从应用角度先来分析找timer/ticker的几个关键点。timer/ticker创建和使用常见的API如下。

创建API
代码语言:javascript
代码运行次数:0
运行
复制
NewTimer(d Duration) *Timer

NewTicker(d Duration) *Ticker

Tick(d Duration) <-chan Time

After(d Duration) <-chan Time

AfterFunc(d Duration, f func()) *Timer
使用API
代码语言:javascript
代码运行次数:0
运行
复制
(t *Timer) Stop() bool

(t *Timer) Reset(d Duration) bool

(t *Ticker) Stop()

<- ticker.C 

<- timer.C

从上面的API可以看到,创建定时器我们需要传入一个触发时间d, 或者触发的函数f, 在使用定时器时,Reset需要传入一个触发时间,timer.C或ticker.C读取的是chan. 总结起来,我们需要关心的是d、f和C三项内容,结合Timer/Ticker的定义,d、f、C对应到数据结构中的是when、f和C.这三项内容我们称它为定时器的三要素。

  • 参数d:定时时间
  • 参数f: 触发动作
  • 字段C: 时间channel, 只读channel

无论是Timer还是Ticker在创建的时候,chan C初始化的是一个大小为1的有缓冲区chan. 我们在使用<-ticker.C 和 <-timer.C是需要关注是否能够读取到数据,否则可能造成卡死。还有一点需要注意的是,chan C字段是一个只读chan, 也就我们只能从里面读取数据。不能向里面发送数据,因为发送数据是runtime中做的,在时间被触发时,会自动向里面发送当前时间。执行Stop、Reset操作会影响哪些内容?会不会关闭关闭chan C。首先说明,无论是Stop操作还是Reset操作都不会关闭chan C. Stop操作不会修改when的值,也不会修改f的值。Reset操作会修改when的值,不会改f的值。上述5中创建定时器相关的接口创建后的定时器when/f/period/arg信息汇总如下

创建方法

when的值

f的值

period的值

arg的值

NewTimer

d

sendTime

未设置

C

NewTicker

d

sendTime

d

C

Tick

d

sendTime

d

C

After

d

sendTime

未设置

C

AfterFunc

d

goFunc

未设置

f

下面分别看看timer和ticker在触发前、触发后,执行Stop、Reset、从通道读取C操作的结果 对*timer对象进行操作得到结果为:

触发时机

Stop

Reset

<-timer.C

未触发即还没到when时间点

返回true,将定时器从四叉堆中移除

返回true,修改定时器的触发时间when

阻塞

已触发即已过when时间点

返回false

返回false,修改定时器的触发时间when

不会阻塞

对*ticker对象进行操作得到结果为:

触发时机

Stop

Reset

<-ticker.C

未触发即还没到when时间点

返回true,将定时器从四叉堆中移除

-

阻塞

已触发即已过when时间点

返回false

-

不会阻塞

说明一点,上述分析了触发前和触发后两个阶段执行各种操作的结构,还有一种情况,即恰好在正在触发即现在在when时间点上,此时会让出处理执行,稍后会执行,即得到结果等价于已触发情况下的结果。

再来分析下f函数,除了AfterFunc是用户传入自定义处理函数外,其他都是f函数都是sendTime,sendTime会向chan C中发送当前时间,但是chan C的大小为1,会不会在发送的时候卡主,答案是不会的,有default兜底。sendTime实现如下

那用户自定义的函数处理会不会卡主,也不会的。time对传入的函数f做成了参数arg, 执行函数f为goFunc,内部启用了一个单独的协程处理f。

timer.Reset存在的问题

下面看几个timer.Reset使用不当引发的问题

问题实例1
代码语言:javascript
代码运行次数:0
运行
复制
func main() {
 tm := time.NewTimer(1)

 tm.Reset(100 * time.Millisecond)

 <-tm.C

 if !tm.Stop() {
  fmt.Println("tm already stop")
  <-tm.C
 }
}

上述程序输出结果为:

代码语言:javascript
代码运行次数:0
运行
复制
tm already stop
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
main.main()

啥?出现死锁了?!。嗯,上面的程序有问题,卡死在了第二个<-tm.C的地方。现在来分析为啥出现卡死,结合前面的知识。可以得到如下分析:第一行定义了一个1纳秒的定时器,很快在第二行进行了Reset操作。先了解一个时间概念,普通PC执行一个指令的时间大概在2~4纳秒。也就是说在执行Reset操作前,tm已经过期了。所以它的通道C已放的有时间数据了。这个放入操作是函数sendTime放入的。具体是怎么调度的,可以结合定时器实现原理剖析学习。tm Reset后的周期是100毫秒,所以也很快被触发。此时,tm.C中已有数据,所以会走到default分支,并没有成功放入数据。接下来执行<-tm.C,将第一次触发放入的时间数据取走,接下了tm.C为空了。最后再执行tm.Stop操作,因为此时tm已经触发了,所以调用会返回false.所以if条件满足,进入内部执行<tm.C.但tm通道里面并没有数据,所以卡死了。

问题实例2
代码语言:javascript
代码运行次数:0
运行
复制
func main() {
 c := make(chan bool)
 go func() {
  for i := 0; i < 3; i++ {
   time.Sleep(time.Second * 5)
   c <- false
  }
  time.Sleep(time.Second * 5)
  c <- true
 }()

 go func() {
  timer := time.NewTimer(time.Second * 3)
  for {
   if !timer.Stop() {
    <-timer.C
   }

   timer.Reset(time.Second * 3)
   select {
   case b := <-c:
    if !b {
     fmt.Println(time.Now(), " recv false, continue")
     continue
    }
    fmt.Println(time.Now(), "recv true, return")
    return
   case <-timer.C:
    fmt.Println(time.Now(), "timer expired")
    continue
   }
  }
 }()

 var i int
 fmt.Scanln(&i)
}

输出结果为:

代码语言:javascript
代码运行次数:0
运行
复制
2021-06-10 22:38:24.479836 +0800 CST m=+3.000928820 timer expired
1

上面的程序中的goroutine也有卡住,是怎么产生的呢?下面进行分析, main函数中启动了两个goroutine, 一个goroutine向chan c里面放入数据,每隔5秒放一次,另一个goroutine从chan c中读取数据,并设置有3秒的超时。第一次的时候,走的是超时逻辑,因为生产者5秒后才放数据,所以第一次循环结束,timer已经超期了。然后又进行新一轮foo-loop, 执行timer.Stop,这时会返回false因为timer已经过期。所以会进入if逻辑。执行<-timer.C操作,但此时timer通道中已没有数据了,在前一轮的case <- timer.C中已消耗掉了。所以消费者卡主了。在来看此时的生产者,c是无缓冲通道,在往里面放数据的时候,发现也放不进去了,因为此时消费者没有处于runable状态,生产者和消费者goroutine都卡住了,只有main goroutine是活动的。输入一个数后,main goroutine退出了。

上述两个实例代码说明不恰当的使用timer.Reset会导致goroutine卡主,其实这是time库的bug.官方也给出了一个没有详细验证的方案,将上述中的

代码语言:javascript
代码运行次数:0
运行
复制
if !timer.Stop() {
 <-timer.C
}

调整为

代码语言:javascript
代码运行次数:0
运行
复制
if !timer.Stop() {
    select{
        case <-timer.C:
        default:
    }
}

修改后,我们在来运行下上述代码,输出结果如下,看起来是正常的。

代码语言:javascript
代码运行次数:0
运行
复制
2021-06-10 06:33:43.996527 +0800 CST m=+3.004332963 timer expired
2021-06-10 06:33:45.994246 +0800 CST m=+5.001993741  recv false, continue
2021-06-10 06:33:48.995812 +0800 CST m=+8.003471870 timer expired
2021-06-10 06:33:50.996816 +0800 CST m=+10.004417793  recv false, continue
2021-06-10 06:33:53.998909 +0800 CST m=+13.006422391 timer expired
2021-06-10 06:33:56.00154 +0800 CST m=+15.008994067  recv false, continue
2021-06-10 06:33:59.004965 +0800 CST m=+18.012330683 timer expired
2021-06-10 06:34:01.005385 +0800 CST m=+20.012691342 recv true, return

上面的方法通过select case defalult, 防止了程序卡死在<-timer.C. 看似是一个不错的方法。但是也有可能存在问题。首先站在timer的角度,我们的应用程序,即这里的消费者goroutine是timer通道C的消费方,运行在一个goroutine中, runtime的调度goroutine或是监控goroutine会向timer通道C中发送数据,调用的是sendTime方法,也就是我们要明白,sendTime执行和<-timer.C的执行没有明确的先后关系。其次,要理解,timer.Stop操作和<-timer.C不是原子操作。timer.Stop的返回值是根据定时器的状态来决定的。结合下面的代码来看, Stop返回false时, 执行函数f,本程序的f就是sendTime还不一定已经运行甚至是运行完成, 在运行f的时候,P上的锁已释放,不能保证没有数据竞争冲突。

因为不能保证顺序关系,当timer已经停止,所以执行timer.Stop会返回false,但此时还未执行f。所以会进入if !timer.Stop()分支内部的default逻辑。当执行完成select defalut操作之后,才执行f。这时timer.C中已有数据了。在执行后面的select的时候立即触发,并不是像我们想的那样会在3秒后触发。

timer使用如何避险

timer.Reset目前还没有理想的解决方案,像上面采用select + default也还是会存在问题的可能。例如在时间粒度很小的时候,ms级别的定时器。总结起来,要合理的使用timer, 才能减少工作中出现的问题。如何合理的使用timer,就是要明白在什么阶段下能调用timer的哪些方法。熟悉掌握下面这张流程图(https://blogtitle.github.io/go-advanced-concurrency-patterns-part-2-timers/),有助于与我们避险。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-06-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 数据小冰 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • timer/ticker三要素
    • 创建API
    • 使用API
  • timer.Reset存在的问题
    • 问题实例1
    • 问题实例2
  • timer使用如何避险
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档