GO定时器原理及使用

  • Post author:
  • Post category:其他




0. 前言

在进行并发编程时,有时候会需要定时功能,比如监控某个GO程是否会运行过长时间、定时打印日志等等。

GO标准库中的定时器主要有两种,一种为Timer定时器,一种为Ticker定时器。

Timer计时器

使用一次后,就失效了,需要Reset()才能再次生效。而

Ticker计时器

会一直生效,接下来分别对两种进行介绍。



1. Timer定时器

首先介绍一下GO定时器的

实现原理



在一个GO进程中,其中的所有计时器都是由一个运行着

timerproc()

函数的 goroutine 来保护。它使用时间堆(最小堆)的算法来保护所有的 Timer,其底层的数据结构基于数组的最小堆,堆顶的元素是间隔超时最近的 Timer,这个 goroutine 会定期 wake up,读取堆顶的 Timer,执行对应的

f

函数或者

sendtime()

函数(下文会对这两个函数进行介绍),而后将其从堆顶移除。

接着看看

Timer的结构

type Timer struct {
    C <-chan Time
    // contains filtered or unexported fields
}

Timer中对外暴露的只有一个channel,这个 channel 也是定时器的核心。当计时结束时,Timer会发送值到channel中,外部环境在这个 channel 收到值的时候,就代表计时器超时了,可与select搭配执行一些超时逻辑。可以通过

time.NewTimer



time.AfterFunc

或者

time.Afte

对一个Timer进行创建。



1.1 time.NewTimer() 和 time.After()



1.1.1 time.NewTimer()

查看以下简单的应用代码:

package main

import (
   "fmt"
   "time"
)
type H struct {
   t *time.Timer
}

func main()  {
   fmt.Println("main")
   h:=H{t: time.NewTimer(1*time.Second)}
   go h.timer()
   time.Sleep(10*time.Second)
}

func (h *H) timer()  {
   for  {
      select {
      case <-h.t.C:
         fmt.Println("timer")
      }
   }

}

我们创建了一个timer,设置时间为1S。然后使用一个select对timer的C进行接受,GO程运行timer()函数,会一直阻塞直到超时发生(接收到C的数据),此时打印timer。



Stop() 停止 Timer

func (t *Timer) Stop() bool


Stop()

是 Timer 的一个方法,调用

Stop()

方法,会停止这个 Timer 的计时,使其失效,之后触发定时事件。

实际上,调用此方法后,此Timer会被从时间堆中移除。



Reset()重置Timer

注意,

Timer定时器超时一次后就不会再次运行,所以需要调用Reset函数进行重置

。修改select中代码,在Case中添加一个重置的代码:

select {
case <-h.t.C:
   fmt.Println("timer")
   h.t.Reset(1*time.Second)
}

可以看到,会不停的打印timer,这是因为使用了Reset函数重置定时器。


注意!不能随意的对Reset方法进行调用

,官网文档中特意强调:

For a Timer created with NewTimer, Reset should be invoked only on stopped or expired timers with drained channels.

大概的意思就是说,除非Timer已经被停止或者超时了,否则不要调用Reset方法,因为,如果这个 Timer 还没超时,不先去Stop它,而是直接Reset,那么旧的 Timer 仍然存在,并且仍可能会触发,会产生一些意料之外的事。所以通常使用如下的代码,安全的重置一个

不知状态

的Timer(以上的代码中,Reset调用时,总是处于超时状态):

if !t.Stop() {
    select {
    case <-h.t.C: 
    default:
    }
}
h.t.Reset(1*time.Second)



1.1.2 time.After()

此方法就像是一个极简版的Timer使用,调用

time.After()

,会直接返回一个channel,当超时后,此channel会接受到一个值,简单使用如下:

package main

import (
	"fmt"
	"time"
)

func main() {
	fmt.Println("main")
	go ticker()
	time.Sleep(100 * time.Second)
}

func ticker() {
	for {
		select {
		case <-time.After(1 * time.Second):
			fmt.Println("timer")
		}
	}
}

注意,此方法虽然简单,但是没有Reset方法来重置定时器,但是可以搭配for 和select的重复调用来模拟重置。



1.1.3 sendtime函数


NewTimer



After

这两种创建方法,会

Timer

在超时后,执行一个标准库中内置的函数:

sendTime

,来将当前的时间发送到channel中。



1.2 time.AfterFunc

此方法可以接受一个func类型参数,在计时结束后,会运行此函数,查看以下代码,猜猜会出现什么结果?

package main

import (
   "fmt"
   "time"
)

func main() {
   fmt.Println("main")
   t := time.AfterFunc(1*time.Second, func() {
      fmt.Println("timer")
   })
   go timer(t)
   time.Sleep(10 * time.Second)
}
func timer(t *time.Timer) {
   select {
   case <-t.C:
      fmt.Println("123")
   }
}

结果只打印了main以及timer。这是因为此方法并不会调用上文提到的sendtime()函数,即不会发送值给Timer的Channel,所以select就会一直阻塞。



f函数

特意将

AfterFunc

和以上的

NewTimer



After

,就是因为f函数的存在。这种方式创建的

Timer

,在到达超时时间后会在单独的

goroutine

里执行函数

f

,而不会执行sendtime函数。

注意,外部传入的

f

参数并非直接运行在

timerproc

中,而是启动了一个新的

goroutine

去执行此方法。



2. Ticker定时器


Ticker

定时器可以周期性地不断地触发时间事件,不需要额外的Reset操作。

其使用方法与Timer大同小异。通过

time.NewTicker

对Ticker进行创建,简单的使用如下:

package main

import (
   "fmt"
   "time"
)

func main() {
   fmt.Println("main")
   t:=time.NewTicker(1*time.Second)
   go timer(t)
   time.Sleep(10 * time.Second)
}
func timer(t *time.Ticker) {
   for{
      select {
      case <-t.C:
         fmt.Println("timer")
      }
   }
}



版权声明:本文为doreen211原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。