首页 > 其他分享 >go timer

go timer

时间:2024-05-28 12:00:21浏览次数:25  
标签:Stop Timer timers timer func time go

Timer的创建

Timer是一次性的时间触发事件,这点与Ticker不同,后者则是按一定时间间隔持续触发时间事件。Timer常见的使用场景如下:

场景1:

t := time.AfterFunc(d, f)

场景2:
select {
    case m := <-c:
       handle(m)
    case <-time.After(5 * time.Minute):
       fmt.Println("timed out")
}

或:
t := time.NewTimer(5 * time.Minute)
select {
    case m := <-c:
       handle(m)
    case <-t.C:
       fmt.Println("timed out")
}

Timer三种创建姿势:

t:= time.NewTimer(d)
t:= time.AfterFunc(d, f)
c:= time.After(d)

time.After跟,time.AfterFunc其中第一个After接口返回一个chan Time, 当时间到时可以读出Timer, AfterFunc接受一个方法,当时间到时执行这个方法。

package main

import (
  "time"
  "fmt"
)

func main() {
  a := time.After(2 * time.Second)
  <- a
  fmt.Println("timer receive")

  time.AfterFunc(2 * time.Second, func(){
    fmt.Println("timer receive")
  })
}

Timer有三个要素:

* 定时时间:也就是那个d
* 触发动作:也就是那个f
* 时间channel: 也就是t.C

内部实现

由于After跟AfterFunc差不多,这里主要看看AfterFunc的实现

  //time/sleep.go
 func AfterFunc(d Duration, f func()) *Timer {
    t := &Timer{
      r: runtimeTimer{
    when: when(d),
    f:    goFunc,
    arg:  f,
      },
    }
    startTimer(&t.r)
    return t
  } 
  func goFunc(arg interface{}, seq uintptr) {
      go arg.(func())()
}

AfterFunc很简单,就是把参数封装为runtimeTimer,然后启动timer(把timer添加到队列中), 这部分代码在runtime/time.go中,注意这里goFunc新启动了一个goroutine来执行用户的任务,这样用户的func就不会堵塞timer

//runtime/time.go
  func startTimer(t *timer) {
    if raceenabled {
      racerelease(unsafe.Pointer(t))
    }
    addtimer(t)
  }

  func addtimer(t *timer) {
    lock(&timers.lock)
    addtimerLocked(t)
    unlock(&timers.lock)
  }

  // Add a timer to the heap and start or kick the timer proc.
  // If the new timer is earlier than any of the others.
  // Timers are locked.
  func addtimerLocked(t *timer) {
    // when must never be negative; otherwise timerproc will overflow
    // during its delta calculation and never expire other runtime·timers.
    if t.when < 0 {
      t.when = 1<<63 - 1
    }
    //添加time到全局timer
    t.i = len(timers.t)
    timers.t = append(timers.t, t)
    //使用最小堆算法维护timer队列
    siftupTimer(t.i)
    //如果是第一个
    if t.i == 0 {
      // siftup moved to top: new earliest deadline.
      //如果在sleep中, 唤醒
      if timers.sleeping {
    timers.sleeping = false
    notewakeup(&timers.waitnote)
      }
      //如果在调度中, 等待
      if timers.rescheduling {
    timers.rescheduling = false
    goready(timers.gp, 0)
      }
    }
    //如果timer还没创建,则创建
    if !timers.created {
      timers.created = true
      go timerproc()
    }
  }

func timerproc() {
  timers.gp = getg()
  for {
    lock(&timers.lock)
    timers.sleeping = false
    now := nanotime()
    delta := int64(-1)
    for {
      if len(timers.t) == 0 {
    delta = -1
    break
      }
      t := timers.t[0]
      //得到剩余时间, 还没到时间就sleep
      delta = t.when - now
      if delta > 0 {
    break
      }
      //如果是周期性的就算下一次时间
      if t.period > 0 {
    // leave in heap but adjust next time to fire
    t.when += t.period * (1 + -delta/t.period)
    //最小堆下沉
    siftdownTimer(0)
      } else {
    // remove from heap
    //删除将要执行的timer,(最小堆算法)
    last := len(timers.t) - 1
    if last > 0 {
      timers.t[0] = timers.t[last]
      timers.t[0].i = 0
    }
    timers.t[last] = nil
    timers.t = timers.t[:last]
    if last > 0 {
      siftdownTimer(0)
    }
    t.i = -1 // mark as removed
      }
      f := t.f
      arg := t.arg
      seq := t.seq
      unlock(&timers.lock)
      if raceenabled {
    raceacquire(unsafe.Pointer(t))
      }
      //执行函数调用函数
      f(arg, seq)
      lock(&timers.lock)
    }
    //继续下一个,因为可能下一个timer也到时间了
    if delta < 0 || faketime > 0 {
      // No timers left - put goroutine to sleep.
      timers.rescheduling = true
      goparkunlock(&timers.lock, "timer goroutine (idle)", traceEvGoBlock, 1)
      continue
    }
    // At least one timer pending.  Sleep until then.
    timers.sleeping = true
    noteclear(&timers.waitnote)
    unlock(&timers.lock)
    //没到时间,睡眠delta时间
    notetsleepg(&timers.waitnote, delta)
  }
}

3 其他实现方法

之前看内核的timer使用的是时间轮的方式

4 API使用

func (t *Timer) Reset(d Duration) bool
Reset使t重新开始计时,(本方法返回后再)等待时间段d过去后到期。如果调用时t还在等待中会返回真;如果t已经到期或者被停止了会返回假。

/	if !t.Stop() {
//		<-t.C
//	}
//	t.Reset(d)
//
// This should not be done concurrent to other receives from the Timer's
// channel.

 func (t *Timer) Stop() bool

time.Timer.C 是一个 chan time.Time 而且在 Stop 时不会关闭,所以在 <-time.Timer.C 的地方如果 Stop 了就会阻塞住。

 To ensure the channel is empty after a call to Stop, check the
// return value and drain the channel.
// For example, assuming the program has not received from t.C already:
//
//	if !t.Stop() {
//		<-t.C
//	}
//

所以:

ctx, cancel := context.WithCancel(context.Background())
timer := time.NewTimer(time.Minute)
timer.Stop()
select {
    // 把上面的 cancel 保存起来可以在其它协程里中断阻塞的定时器
    case <-ctx.Done():
    // 这里会无限阻塞
    case <-timer.C:
}

按照 Timer.Stop 文档 的说法,每次调用 Stop 后需要判断返回值,如果返回 false(表示 Stop 失败,Timer 已经在 Stop 前到期)则需要排掉(drain)channel 中的事件

if !t.Stop() {
	<-t.C
}

但是如果之前程序已经从 channel 中接收过事件,那么上述 <-t.C 就会发生阻塞。可能的解决办法是借助 select 进行 非阻塞 排放(draining):

if !t.Stop() {
	select {
	case <-t.C: // try to drain the channel
	default:
	}
}

使用 Timer 的正确方式

参考 https://github.com/golang/go/issues/11513#issuecomment-157062583   和 https://groups.google.com/g/golang-dev/c/c9UUfASVPoU/m/tlbK2BpFEwAJ     ,目前 Timer 唯一合理的使用方式是:

  • 程序始终在同一个 goroutine 中进行 Timer 的 Stop、Reset 和 receive/drain channel 操作
  • 程序需要维护一个状态变量,用于记录它是否已经从 channel 中接收过事件,进而作为 Stop 中 draining 操作的判断依据

参考:

 

标签:Stop,Timer,timers,timer,func,time,go
From: https://www.cnblogs.com/codestack/p/18217643

相关文章

  • 【转载】从零开始的硬件之路14:解决AD工程文件过大问题及运行AD插件导入Logo
    原文链接:https://zhuanlan.zhihu.com/p/397285331 这篇讲两个内容,分别是”解决AD工程文件过大问题“和”运行AD插件导入图形符号“。目录:AD工程文件过大问题运行AD插件导入Logo首先何为工程文件过大,来图直接说明:可以看到一个工程文件占用了两百多的内存(我以前的一个......
  • Red is good
    Description桌面上有R张红牌和B张黑牌,随机打乱顺序后放在桌面上,开始一张一张地翻牌,翻到红牌得到1美元,黑牌则付出1美元。可以随时停止翻牌,在最优策略下平均能得到多少钱。Input一行输入两个数R,B,其值在0到5000之间Output在最优策略下平均能得到多少钱。解析设计状态:\(f[i]......
  • Django 接收用户请求并通过HTTP回应
    准备工作python版本:3.10(本人的)Django版本:3.2.12(LTS长期支持版)注意:不同Django所对应的python版本是有要求的,建议事先查找自己的python版本,Django建议下载LTS长期支持版的安装:python3 //查看版本(在window用python命令)sudopip3installdjango[版......
  • 【解决办法】RegularPolygon.__init__() takes 3 positional arguments but 4 were gi
    我在学习用Python绘制一个六边形且隐藏全部轴脊的代码时,出现如下报错:RegularPolygon._init_()takes3positionalargumentsbut4weregiven报错意思:RegularPolygon.__init__()接受3个位置参数,但给定了4个通过上网查询、询问同学,我解决了这个问题,其中的解决过程我详细地......
  • golang为什么chan大部分是发送结构体,而不是其它比如string
     typetokenstruct{}typeGroupstruct{cancelfunc(error)wgsync.WaitGroupsemchantokenerrOncesync.Onceerrerror}func(g*Group)done(){ifg.sem!=nil{<-g.sem}g.wg.Done()}在Go语言中,通道(......
  • golang的交叉编译是什么
     Go(Golang)的交叉编译是指在一种硬件架构或操作系统环境下,使用Go编译器生成适用于另一种架构或操作系统的可执行程序。Go语言的设计使得交叉编译变得非常简单和高效,它允许开发者在开发环境中构建目标平台上的代码,而无需在目标平台上实际运行编译过程。 在Go中,交叉编译主要涉......
  • golang的 CGO 是什么
     CGO是Go(Golang)语言中的一个工具,全称为"C-Go"或者"CforGo"。它是Go标准库的一部分,允许Go代码与C语言代码进行交互。CGO提供了在Go程序中使用C语言库的能力,同时也允许C代码调用Go的函数。通过CGO,开发者可以利用Go语言的强类型和垃圾回收等特性,同时利用C语言的高性能和广......
  • Go 运算符与表达式
    Go运算符与表达式上一篇:Go基本数据类型下一篇:Go控制结构文章目录Go运算符与表达式前言一运算符二算术运算符2.1算术运算符案例三赋值运算符四比较运算符五逻辑运算符六位运算符七表达式与表达式求值总结前言在上一篇,我们介绍了Go提供的基本类......
  • Django框架前后端通信打通实战(PyCharm高级版)
    1.创建django项目并做好相关配置首先在pycharm高级版中创建django项目(1)选择左上角的四条小横线,然后找到文件下面的新建项目并点击,如下图:(2)点击完上图的新建项目之后,来到下面的页面. 然后点击左上角的Django,然后设置文件的位置,之后将模版文件夹的template这个单......
  • .NET 7 AOT 的使用以及 .NET 与 Go 互相调用
    目录背景C#部分环境要求创建一个控制台项目体验AOT编译C#调用库函数减少体积C#导出函数C#调用C#生成的AOTGolang部分安装GCCGolang导出函数.NETC#和Golang互调C#调用GolangGolang调用C#其他 背景其实,规划这篇文章有一段时......