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

1.简介

首先展示基于

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

2.基本使用

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

NewTimer
实现超时控制的。

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

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

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

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
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
进行监听的协程,此时也正常接收到通知了。