«

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

    热门推荐