当前位置:   article > 正文

深入解析go Timer 和Ticker实现原理_golang tricker

golang tricker

1 timer

timer 简单来说就是1 个定时器,代表多少秒后执行,当创建1个timer,1秒钟过后,我们就能从timer.C 获取那个时刻的时间,因为系统在那个时刻将当前时间写入到timer.C 了,这时候我们就可以做自己的想做的事了。

  1. package main
  2. import (
  3. "fmt"
  4. "time"
  5. )
  6. func main() {
  7. timer:=time.NewTimer(1*time.Second)
  8. defer timer.Stop()
  9. msg:=<-timer.C
  10. fmt.Println("1秒后打印",msg)
  11. }
  12. //结果:1秒后打印 2021-11-02 22:43:42.2260892 +0800 CST m=+1.016042901

timer 结构

  1. type Timer struct {
  2. C <-chan Time //存储时间的管道
  3. r runtimeTimer //底层存储timer 的堆实现
  4. }
  • timer 代表单一的事件
  • 当timer 过期后,当前时间将会被发送到C
  • timer 只能被NewTimer 或者 AfterFunc两个函数创建

runtimeTimer结构

  1. type runtimeTimer struct {
  2. pp uintptr
  3. when int64 //什么时候触发timer
  4. period int64 //如果是周期性任务,执行周期性任务的时间间隔
  5. f func(interface{}, uintptr) // NOTE: must not be closure//到时候执行的回调函数
  6. arg interface{} //执行任务的参数
  7. seq uintptr//回调函数的参数,该参数仅在 netpoll 的应用场景下使用。
  8. nextwhen int64//如果是周期性任务,下次执行任务时间
  9. status uint32//timer 的状态
  10. }

p 上存储timer 的结构

下面只展示跟timer 有关的字段

  1. //Go\src\runtime\runtime2.go +604
  2. type p struct {
  3. .....
  4. //堆顶元素什么时候执行
  5. timer0When uint64
  6. //如果有timer 修改为更早执行时间了,将会将执行时间更新到当更早时间
  7. timerModifiedEarliest uint64
  8. //操作timer 的互斥锁
  9. timersLock mutex
  10. //该p 上的所有timer,必须加锁去操作这个字段,因为不同的p 操作这个字段会有竞争关系
  11. timers []*timer
  12. //p 堆上所有的timer
  13. numTimers uint32
  14. //被标记为删除的timer,要么是我们调用stop,要么是timer 自己触发后过期导致的删除
  15. deletedTimers uint32
  16. ....
  17. }

为什么是四叉堆

但是与我们常见的,使用二叉树来实现最小堆不同,Golang 这里采用了四叉堆 (4-heap) 来实现。这里 Golang 并没有直接给出解释。 这里直接贴一段 知乎网友对二叉堆和 N 叉堆的分析

  1. 上推节点的操作更快。假如最下层某个节点的值被修改为最小,同样上推到堆顶的操作,N 叉堆需要的比较次数只有二叉堆的

倍。

    1. 对缓存更友好。二叉堆对数组的访问范围更大,更加随机,而 N 叉堆则更集中于数组的前部,这就对缓存更加友好,有利于提高性能。

C 语言知名开源网络库 libev,其timer定时器实现可以在编译时选择采用四叉堆。在它的注释里提到四叉树相比来说缓存更加友好。 根据benchmark,在 50000 + 个 timer 的场景下,四叉树会有 5% 的性能优势。具体可见 libev/ev.c#L2227

NewTimer

  1. //创建一个将会在Duration 时间后将那一刻的时间发生到C 的timer
  2. func NewTimer(d Duration) *Timer {
  3. c := make(chan Time, 1) //创建1个channel
  4. t := &Timer{ //创建一个timer
  5. C: c,
  6. r: runtimeTimer{
  7. when: when(d), //什么时候执行
  8. f: sendTime, //到时候执行的回调函数
  9. arg: c,//执行参数
  10. },
  11. }
  12. startTimer(&t.r) //开始timer
  13. return t
  14. }
  • C 是一个带1个容量的chan,这样做有什么好处呢,原因是chan 无缓冲发送数据就会阻塞,阻塞系统协程,这显然是不行的。
  • 回调函数设置为sendTime,执行参数为channel,sendTime就是到点往C 里面发送当前时间的函数

sendTime实现

  1. //c interface{} 就是NewTimer 赋值的参数,就是channel
  2. func sendTime(c interface{}, seq uintptr) {
  3. select {
  4. case c.(chan Time) <- Now(): //写不进去的话,C 已满,走default 分支
  5. default:
  6. }
  7. }
  • sendTime 是不阻塞的,在Timer 实现里面是不会被阻塞的,因为只写一次数据。但是在Ticker里面就会存在阻塞,因为容量为1,ticker 会按时间间隔周期性的写数据到C,这时候如果没有写进去,这次写事件就会丢弃。那么是怎么做到呢?
    case c.(chan Time) <- Now() 的时候,如果C 里面的数据没人取走,那么C 已满,case 这条分支发送数据到C就会执行失败而走下面的default。相当于本次调用没有任何操作。
  • 官方注释说:如果reader读C数据慢于第二次向C写数据,那么丢掉这次数据是理想的行为。

After

和NewTimer 一样 ,只是NewTimer 的语法糖,将创建的timer 的C返回了,这样就可以直接使用了

  1. func After(d Duration) <-chan Time {
  2. return NewTimer(d).C
  3. }
  • 和 NewTimer(d).C 是等价的,等待Duration 后将触发,将当前时间写入到返回的 channel。
  • 在计时器触发前,GC 是不会回收timer 的(内存泄漏的提醒,遇到问题时不要怪go 坑,官方可有说明~///(^v^)\\~),不再需要timer时,调用NewTimer 创建timer,然后去调用Timer.Stop效率更高。

AfterFunc

这个函数代表在多少秒后执行传入的回调函数f,并返回timer

  1. func AfterFunc(d Duration, f func()) *Timer {
  2. t := &Timer{
  3. r: runtimeTimer{
  4. when: when(d),//什么时候触发
  5. f: goFunc, //到时候执行的回调函数
  6. arg: f//参数为回调函数
  7. },
  8. }
  9. startTimer(&t.r)
  10. return t
  11. }
  12. //执行的回调函数
  13. func goFunc(arg interface{}, seq uintptr) {
  14. go arg.(func())()
  15. }
  • AfterFunc 等待Duration 后触发,在自己goroutine 触发执行回调函数f,返回可以通过调用stop 去放弃执行的timer。
  • 最终执行的函数是goFunc,参数为用户传入的回调函数,当时间一到,启动goroutine 去执行任务。所以用这个执行回调函数修改共享数据很可能产生并发问题。

stop

  1. func (t *Timer) Stop() bool {
  2. if t.r.f == nil {
  3. panic("time: Stop called on uninitialized Timer")
  4. }
  5. return stopTimer(&t.r) //调用系统的 stopTimer
  6. }
  • 从激活中阻止timer
  • 当timer stop 时返回true,如果timer 已经过期(就是已经被激活)或者已经停止那么则返回false
  1. package main
  2. import (
  3. "fmt"
  4. "time"
  5. )
  6. //timer 正常stop
  7. func Test1() {
  8. timer:=time.NewTimer(1*time.Second)
  9. fmt.Println(timer.Stop()) //true
  10. }
  11. //timer 已调用stop
  12. func Test2() {
  13. timer:=time.NewTimer(1*time.Second)
  14. timer.Stop()
  15. fmt.Println(timer.Stop()) //false
  16. }
  17. //timer 已被激活
  18. func Test3() {
  19. timer:=time.NewTimer(1*time.Second)
  20. <-timer.C
  21. fmt.Println(timer.Stop())//false
  22. }
  23. func main() {
  24. Test1()
  25. Test2()
  26. Test3()
  27. }
  • stop 不会关闭channel,去阻止从channel 成功的读取是完全不正确的
  • 确认channel 是空的在调用stop 后,检查返回值然后排空channel.
    例如:假设t.c 里面的值还没被接收
  1. if !t.Stop() {
  2. <-t.C
  3. }
  • 注意事项:不能并发的从channel 里面读数据,也不能并发的调用stop ,我们来看看会发生什么事。因为并发的读chan,当chan 读完1 个数据以后,没有发送者了就会一直阻塞,造成死锁
  1. package main
  2. import (
  3. "time"
  4. )
  5. func main() {
  6. timer:=time.NewTimer(1*time.Second)
  7. for i:=0;i<10;i++{
  8. <-timer.C
  9. }
  10. }
  11. //结果
  12. //fatal error: all goroutines are asleep - deadlock!
  13. goroutine 1 [chan receive]:
  14. main.main()
  15. D:/code/leetcode/timer.go:10 +0x59

来看看并发的调用stop

  1. package main
  2. import (
  3. "fmt"
  4. "time"
  5. )
  6. func main() {
  7. timer:=time.NewTimer(1*time.Second)
  8. for i:=0;i<100;i++{
  9. go fmt.Println(timer.Stop())
  10. }
  11. }
  12. //虽然没有错误产生,但是也是不可取的,多次执行返回的状态却不一定是正确的.
  13. false
  14. false
  15. false
  16. true //顺序不固定,多次执行结果不一样
  17. false
  • AfterFunc(d, f) 创建的timer 调用t.stop 返回false ,说明timer 过期了,f 在自己的goroutine 开始执行了,并且stop 不会等待f 执行完才返回,如果想知道f 是否已经完成,则需要自己明确的协商定义,自己去获取状态,例如下面的操作。根据结果可以看到,当timer 已经激活,timer.Stop返回false,但是f 并发没有执行完,我们如果想知道f 的状态,则需要自己操作获取状态。
  1. package main
  2. import (
  3. "fmt"
  4. "time"
  5. )
  6. func main() {
  7. var isComplete bool
  8. timer:=time.AfterFunc(1*time.Second, func() {
  9. time.Sleep(5*time.Second)
  10. fmt.Println("任务完成")
  11. isComplete=true //此例子不是并发操作
  12. })
  13. time.Sleep(2*time.Second) //让timer 过期激活
  14. fmt.Println(timer.Stop(),"isComplete=",isComplete) //true isComplete= false
  15. for {
  16. if isComplete{
  17. break
  18. }
  19. }
  20. fmt.Println("任务完成",isComplete)
  21. }
  22. //下面是执行结果
  23. //false isComplete= false
  24. //任务完成
  25. //任务完成 true

2 ticker

Ticker 形容时钟滴答滴答的声音,在go 中常用来做定时任务,任务到了执行任务。

Ticker 使用案例,常用来做定时任务或者顶层连接心跳,每秒定时做什么

  1. package main
  2. import (
  3. "fmt"
  4. "time"
  5. )
  6. func main() {
  7. t:=time.NewTicker(1*time.Second)
  8. defer t.Stop()
  9. for now:=range t.C{
  10. fmt.Println(now)
  11. }
  12. }

Ticker 结构

  1. type Ticker struct {
  2. C <-chan Time //chan 定时到了以后,go 系统会忘里面添加一个当前时间的数据
  3. r runtimeTimer
  4. }

创建一个Ticker

  1. func NewTicker(d Duration) *Ticker {
  2. if d <= 0 {
  3. panic(errors.New("non-positive interval for NewTicker"))
  4. }
  5. //这里预留一个缓冲给timer 一样,但是满了以后没人接收后面会丢掉事件
  6. c := make(chan Time, 1)
  7. t := &Ticker{
  8. C: c,
  9. r: runtimeTimer{
  10. when: when(d),
  11. period: int64(d),
  12. f: sendTime, //和ticker 的函数一样
  13. arg: c,
  14. },
  15. }
  16. startTimer(&t.r)
  17. return t
  18. }
  • 和timer 创建方式一样,只不过period为Duration,这样底层在检查时会根据这个字段判断是不是周期性timer,从而删掉原来的timer,创建新的timer

stop

调用stopTimer停止ticker,停止不会关闭 channel。channel也不能被并发读

  1. func (t *Ticker) Stop() {
  2. stopTimer(&t.r)
  3. }

Reset

调用modTimer修改时间,接下来的激活将在新period后

  1. func (t *Ticker) Reset(d Duration) {
  2. if t.r.f == nil {
  3. panic("time: Reset called on uninitialized Ticker")
  4. }
  5. modTimer(&t.r, when(d), int64(d), t.r.f, t.r.arg, t.r.seq)
  6. }

Tick

  • 返回ticker 的channel,和timer 一样,对于不需要关闭timer 的客户端有用,但是注意这里没有一个方式去关闭底层timer,所以也不会被垃圾回收
  1. func Tick(d Duration) <-chan Time {
  2. if d <= 0 {
  3. return nil
  4. }
  5. return NewTicker(d).C
  6. }

3 sleep

  1. func timeSleep(ns int64) {
  2. if ns <= 0 {
  3. return
  4. }
  5. gp := getg()
  6. t := gp.timer
  7. if t == nil {
  8. t = new(timer)
  9. gp.timer = t
  10. }
  11. t.f = goroutineReady
  12. t.arg = gp
  13. t.nextwhen = nanotime() + ns
  14. if t.nextwhen < 0 { // check for overflow.
  15. t.nextwhen = maxWhen
  16. }
  17. gopark(resetForSleep, unsafe.Pointer(t), waitReasonSleep, traceEvGoSleep, 1)
  18. }
  19. func resetForSleep(gp *g, ut unsafe.Pointer) bool {
  20. t := (*timer)(ut)
  21. resettimer(t, t.nextwhen)
  22. return true
  23. }
  • sleep底层也是创建timer,执行函数为goroutineReady,参数为当前g,执行时间为当前时间+传进来的时间,然后调用gopark。停止协程,等待timer 过期被唤醒。
  • resetForSleep 在goroutine停止后调用,我们不能自己调用resettimer,因为如果这是短暂的sleep,并且有大量goroutines,p 可能在goroutine被停止前提交调用goroutineReady

4 底层源码解析

注意:本文为go 在1.17.2 下面的源码解析,如果是1.15 或者1.16 可能有些不同

来看看下面这些函数,go/src/time 是找不到任何实现的,其实go 官方包

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