«

Golang中NewTimer计时器的底层实现原理是什么

时间:2024-5-19 09:48     作者:韩俊     分类: Go语言


本篇内容介绍了“Golang中NewTimer计时器的底层实现原理是什么”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!

    1.简介

    首先展示基于

    NewTimer
    创建的定时器来实现超时控制。接着通过一系列问题的跟进,展示了
    NewTimer
    的底层实现原理。

    2.基本使用

    我们首先通过一个简单的例子,来展示是怎么基于

    NewTimer
    实现超时控制的。

    假设有一个需求,要求在 5 秒钟内完成某个任务,否则就认为任务失败。这时我们就可以使用

    NewTimer
    来实现超时控制。具体的实现步骤如下:

      首先,基于

      NewTimer
      创建一个 Timer 对象,设定超时时间为 5 秒钟。

      然后,启动一个 goroutine 来执行任务,并在任务完成后向通道发送一个完成信号。

      最后,使用 select 语句等待 Timer 的到期事件或任务完成信号,如果 Timer 先到期,就认为任务超时了,否则就认为任务完成了。

    下面是一个简单的实现代码展示:

    package main
    
    import (
            "fmt"
            "time"
    )
    
    func main() {
            // 创建一个定时器,超时时间为 5 秒钟
            timer := time.NewTimer(5 * time.Second)
    
            // 启动一个 goroutine 执行任务,并在任务完成后向通道发送一个完成信号
            done := make(chan bool, 1)
            go func() {
                    // 模拟任务执行耗时
                    time.Sleep(2 * time.Second)
                    done <- true
            }()
    
            // 等待任务完成或者超时
            select {
            case <-done:
                    // 任务完成,输出完成信息
                    fmt.Println("Task finished.")
            case <-timer.C:
                    // 超时,输出超时信息
                   fmt.Println("Task timed out.")
            }
    }

    在上述代码中,我们首先使用

    NewTimer
    创建了一个
    time.Timer
    对象,超时时间为 5 秒钟。然后启动一个 goroutine 来执行任务,并在任务完成后向通道
    done
    发送一个完成信号。最后使用
     select
    语句等待任务完成信号或者
    Timer
    的到期事件,如果
    Timer
    先到期,就认为任务超时了,否则就认为任务完成了。 在运行上述代码时,我们可以看到,在任务完成前 5 秒钟内,程序输出如下信息:

    Task finished.

    如果将任务完成时间改为超过 5 秒钟,程序将会在 5 秒钟后超时,输出如下信息:

    Task timed out.

    通过这个简单的例子,我们可以看到,如果任务在指定超时时间内完成,此时会执行正常的业务逻辑;如果任务未在指定的超时时间内完成,此时将走执行超时逻辑。

    通过上述程序演示,我们展示了如何使用

    NewTimer
    创建的
    time.Timer
    实现超时控制的基本方法。

    3.实现原理

    3.1 内容分析

    回顾上面的示例代码,我们实现超时控制的主要机制,是通过

    select
    语句同时监听两个
    channel
    ,一个是任务执行状态的
    channel
    ,一个是定时器的
    channel
    。

    当任务执行完成时,便通过

    channel
    对主协程进行通知。当定时器到达我们指定的时间,也就是超时时间,此时也通过定时器的
    channel
    进行通知。

     同时监听这两个

    channel
    ,如果任务先执行完成,此时将会走
    select
    语句中正常业务逻辑
    case
    的代码,如果是在到达预定时间,任务仍没有完成,此时通过定时器
    channel
    进行通知,从而走超时业务逻辑
    case
    的代码,从而实现超时控制。

    因此,这里主要的问题是,是如何在到达超时时间时,准时往定时器中的

    C
    对应的
    channel
    发送送数据,从而来告知其他协程,已经到达超时时间了呢,这个是如何做到的呢?

    3.2 基本思路

    下面先来看看

    NewTimer
    方法返回
    Timer
    结构体的内容,定义如下:

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

    可以看到,

    Timer
    结构体中C是一个
    chan Time
    类型的变量。在前面的代码的例子中,
    select
    语句是监听
    Timer
    结构体中的
    C
    变量,从中来读取数据的。

    那么,当到达超时时间时,

    Timer
    中
    C
    对应的
    channel
    将会有数据到达,那么肯定有其他地方,在到达超时时间时,会往
    Timer
    中的
    C
    发送数据。

    那么现在的主要问题,是怎么做到当到达指定时间时,往

    Timer
    中的
    C
    发送数据呢?

    其实,在

    go
    语言中,存在这样一个运行时函数
    startTimer
    ,定义如下:

    func startTimer(*runtimeTimer)

    它的作用是启动一个定时器,当定时器到期时,会执行相应的回调函数并传递回调参数。在

    startTimer
    函数内部,会使用系统调用来启动一个底层的操作系统定时器,等到定时器超时时,底层系统会自动触发一个信号(例如 Unix 平台上的 SIGALRM 信号),然后该信号将由 Go 运行时内部的信号处理函数捕获,并最终调用相关的回调函数。

    那么,这里我们似乎可以使用

    startTimer
    来实现,当到达指定时间时,往
    channel
    发送数据,从而达到通知其他协程的效果。

    3.3 实现步骤

    首先,我们已经知道,

    startTimer
    能够启动一个定时器,当定时器到期时,会执行相应的回调函数并传递回调参数。而定时器的到期时间、回调函数以及回调函数的参数,则是通过
    runtimeTimer
    结构体传递过去的。

    下面我们只需要

    runtimeTimer
    字段的含义,然后根据其含义,正确设置
    runtimeTimer
    结构体字段,调用
    startTimer
    方法启动一个定时器,就能够实现在指定时间时,执行某段逻辑。下面我们来看看
    runtimeTime
    的定义:

    type runtimeTimer struct {
       pp       uintptr
       when     int64
       period   int64
       f        func(any, uintptr) // NOTE: must not be closure
       arg      any
       seq      uintptr
       nextwhen int64
       status   uint32
    }

    下面对

    runtimeTimer
    中的字段进行说明:

      when int64
      :表示定时器应该在何时触发。该值的单位是纳秒

      period int64
      :表示定时器的重复周期。如果定时器不需要重复触发,则该值为 0

      f func(any, uintptr)
      :指向定时器到期后需要执行的函数。

      arg any
      :表示传递给定时器到期后执行的函数的参数,将传递给
      f
      的第一个参数

      nextwhen int64
      :如果定时器是重复触发的,则
      nextwhen
      表示下一次触发的时间。

      seq uintptr
      :用于防止定时器被错误的重置。当定时器被重置时,
      seq
      会被更新。

    基于对上面字段含义的理解,此时我们定义一个

    runtimeTimer
    结构体,然后调用
    startTimer
    ,从而来实现能够在指定的某个时间点,往某个
    channel
    发送数据。具体实现如下:

    // 定义一个channel,用于发送数据
    c := make(chan Time, 1)
    r := runtimeTimer{
          // 指定超时时间戳
          when: when(d),
          // 指定回调函数
          f: func sendTime(c any, seq uintptr) {
                   select {
                   case c.(chan Time) <- Now():
                   default:
                  }
            },
          // 传递给回调函数的参数
          arg:  c,
       },
    }
    // 调用startTimer启动一个定时器
    startTimer(&t.r)

    首先会创建一个带有缓冲的通道

    c
    。

    接着初始化

    runtimeTimer
    结构体的值,设定好超时时间,回调函数以及参数。超时时间使用的是
    when
    函数来获取计数器的结束时间。
    when
    函数会根据给定的时间间隔 d,返回一个绝对时间点,即计时器结束时间。

    f
    字段指定的回调函数,则是将当前时间
    Now()
    发送到通道
    c
    中。当到达指定超时时间时,其将会调用回调函数
    f
    ,同时将
    runtimeTimer
    结构体中
    arg
    字段的值,作为参数传递到回调函数当中。

    然后调用

    startTimer
    启动一个定时器。当到达超时时间,将会调用回调函数,回调函数其会往一开始定义的
    channel
    发送数据。

    至此,我们实现了最开始提到的,当到达指定时间时,往

    Timer
    中的
    C
    发送数据这个任务。

    3.4 NewTimer的实现

    回到我们的题目,NewTimer计时器的底层实现原理是什么?事实上,

    NewTimer
    创建的定时器,也确实是基于
    startTimer
    来实现的,下面我们来看看其实现:

    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
    }

    首先会创建一个带有缓冲的通道

    c
    。然后创建一个
    Timer
    对象
    t
    ,将
    c
    通道赋值给
    t
    的
    C
    属性。
    channel
    之后将作为回调函数的参数,同时也会作为
    Timer
    对象中
    C
    属性的值。这样子回调函数和
    Timer
    结构体中
    C
    变量与回调函数的
    channel
    事实上是共用一个
    channel
    的。

    而

    runtimeTimer
    结构体中的回调函数
    sendTime
    的实现与之前讲述的并无差异,都是将当前时间
    Now()
    发送到通道中,这里将不再赘述。

    而

    Timer
    结构体中
    C
    变量与回调函数的
    channel
    事实上是共用一个
    channel
    的,当到达超时时间,则会执行回调函数,往
    channel
    发送数据。而通过
    select
    语句对
    Timer
    中的
    channel
    进行监听的协程,此时也正常接收到通知了。

    标签: golang

    热门推荐