golang定时器相关的函数超硬核解析

发布时间 2024-01-04 08:04:31作者: 技术颜良

golang定时器相关的函数超硬核解析

一、前言

Golang 定时器包括:一次性定时器(Timer)和周期性定时器(Ticker)。

编程中经常会通过timer和ticker、AfterFunc。定时器NewTicker是设定每隔多长时间触发的,是连续触发,而计时器NewTimer是等待多长时间触发的,只触发一次,两者是不同的。等待时间函数AfterFunc是在After基础上加了一个回调函数,是等待时间到来后在另外一个goroutine协程里调用。

1.1 定时器相关得函数

  • 1、time.NewTicker():创建一个Ticker类型的定时器。

  • 2、time.Ticker.C:返回一个定时的通道,每隔一段时间发送一个时间值。

  • 3、time.Ticker.Stop():停止定时器。

  • 4、time.NewTimer():创建一个Timer类型的定时器。

  • 5、time.Timer.C:返回一个通道,定时器到期后发送一个时间值。

  • 6、time.Timer.Reset():重新设置定时器到期时间。

  • 7、time.Timer.Stop():停止定时器。

 

二、详细说明

2.1 Timer

2.1.1 说明

timer创建有两种方式,time.NewTimer(Duration) 和time.After(Duration)。后者只是对前者的一个包装。

timer到固定时间后会执行一次,请注意是一次,而不是多次。但是可以通过reset来实现每隔固定时间段执行。使用timer定时器,超时后需要重置,才能继续触发。

2.1.2 Timer案例

import (  "fmt"
"time")
func main() { myTimer := time.NewTimer(time.Second * 10) // 启动定时器 var i int = 0 select { case <-myTimer.C: i++    fmt.Println("count: ", i) }}

2.1.3 Timer数据结构

type Timer struct {    C <-chan Time // 抛出来的channel,给上层系统使用,实现定时    r runtimeTimer // 给系统管理使用的定时器,系统通过该字段确定定时器是否到时,如果到时,调用对应的函数向C中推送当前时间。}
  • 当timer 过期后,当前时间将会被发送到C

  • timer 只能被NewTimer 或者 AfterFunc两个函数创建

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

那么定时器是如何实现的呢?首先看一下定时器的构造:

//创建一个将会在Duration 时间后将那一刻的时间发生到C 的timerfunc NewTimer(d Duration) *Timer {   c := make(chan Time, 1)  //创建1个channel   t := &Timer{ //创建一个timer      C: c,      r: runtimeTimer{         when: when(d), //什么时候执行         f:    sendTime,  //到时候执行的回调函数         arg:  c,//执行参数      },   }   startTimer(&t.r) //开始timer   return t}
  • C 是一个带1个容量的chan,这样做有什么好处呢,原因是chan 无缓冲发送数据就会阻塞,阻塞系统协程,这显然是不行的。

  • 回调函数设置为sendTime,执行参数为channel,sendTime就是到点往C 里面发送当前时间的函数

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

  • 官方注释说:如果reader读C数据慢于第二次向C写数据,那么丢掉这次数据是理想的行为。

2.2 Ticker

2.2.1 说明

ticker只要定义完成,从此刻开始计时,不需要任何其他的操作,每隔固定时间都会触发。它会以一个间隔(interval)往通道发送当前时间,而通道的接收者可以以固定的时间间隔从通道中读取时间。

2.2.2 案例说明

package main
import ( "fmt" "time")
func main() { t:=time.NewTicker(1*time.Second) defer t.Stop() for now:=range t.C{ fmt.Println(now) }}

周期性定时器到期了之后同样是执行sendTime方法,这个上面已经描述过了。细心的你肯定注意到了,在tickerDemo中有一个defer去停止ticker,为什么要这么做呢?前面分析的时候讲到,创建定时器就是把定时器的runtimeTimer放到由维护协程维护的堆中,一次性定时器到期后,会从堆中删除,如果没有到期则调用Stop方法实现删除。但是,周期性定时器是不会执行删除动作的,所以如果项目里面持续创建多个周期性定时器并没有stop的话,会导致堆越来越大,从而引起资源泄露。

经过代码验证:time.NewTicker定时触发执行任务,当下一次执行到来而当前任务还没有执行结束时,会等待当前任务执行完毕后再执行下一次任务。

2.2.3 数据结构

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

创建一个Ticker

func NewTicker(d Duration) *Ticker {   if d <= 0 {      panic(errors.New("non-positive interval for NewTicker"))   }  //这里预留一个缓冲给timer 一样,但是满了以后没人接收后面会丢掉事件   c := make(chan Time, 1)   t := &Ticker{      C: c,      r: runtimeTimer{         when:   when(d),         period: int64(d),         f:      sendTime, //和ticker 的函数一样         arg:    c,      },   }   startTimer(&t.r)   return t}

和timer 创建方式一样,只不过period为Duration,这样底层在检查时会根据这个字段判断是不是周期性timer,从而删掉原来的timer,创建新的timer

2.3 总结

2.3.1 特殊说明

上面的两种计时器都会在底层创建一个runtimeTimer,所以每一个版本中runtimeTimer的优化都十分重要

  • Go 1.9版本之前,使用全局唯一的四叉堆维护

  • Go 1.10-1.13,全局使用64个四叉堆,每个处理器(P)对应一个四叉堆

  • Go 1.14版本之后,每个处理器P直接管理一个四叉堆,通过网络轮询器触发

 

2.3.2 四叉堆说明

图片

|希望能帮助你学习到新的知识点,欢迎提出宝贵的意见!!

golang面试经典讲解
后端技术面试交流,以常见面试题讲解golang知识、Mysql、Redis、网络、Linux、微服务架构以及微服务治理相关面试知识,让大家面试游刃有余,offer拿到手软,大家也可以后台提交相关面试题,我会统一输出供大家一起成长
54篇原创内容
go · 目录
上一篇用Go来开发WebAssembly入门
个人观点,仅供参考
阅读 194