添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

Go语言的定时器实质是单向通道,time.Timer结构体类型中有一个time.Time类型的单向chan,源码(src/time/time.go)如下

type Timer struct {
C <-chan Time
r runtimeTimer

初始化 Timer 方法为NewTimer

package main
import (
    "fmt"
    "time"
func main() {
    t := time.NewTimer(time.Second * 2)
    defer t.Stop()
    for {
        <-t.C
        fmt.Println("timer running...")
        // 需要重置Reset 使 t 重新开始计时
        t.Reset(time.Second * 2)

输出
timer running…
timer running…
timer running…
timer running…
这里使用NewTimer定时器需要t.Reset重置计数时间才能接着执行。如果注释 t.Reset(time.Second * 2)会导致通道堵塞,报fatal error: all goroutines are asleep - deadlock!错误。

同时需要注意 defer t.Stop()在这里并不会停止定时器。这是因为Stop会停止Timer,停止后,Timer不会再被发送,但是Stop不会关闭通道,防止读取通道发生错误。

t := time.NewTimer(time.Second * 2)
ch := make(chan bool)
go func(t *time.Timer) {
    defer t.Stop()
    for {
        select {
        case <-t.C:
            fmt.Println("timer running....")
            // 需要重置Reset 使 t 重新开始计时
            t.Reset(time.Second * 2)
        case stop := <-ch:
            if stop {
                fmt.Println("timer Stop")
                return
time.Sleep(10 * time.Second)
ch <- true
close(ch)
time.Sleep(1 * time.Second)

time.After
time.After()表示多长时间长的时候后返回一条time.Time类型的通道消息。但是在取出channel内容之前不阻塞,后续程序可以继续执行。

先看源码(src/time/sleep.go)

func After(d Duration) <-chan Time {
   return NewTimer(d).C
}
 
通过源码我们发现它返回的是一个NewTimer(d).C,其底层是用NewTimer实现的,所以如果考虑到效率低,可以直接自己调用NewTimer。

package main
import (
   "fmt"
   "time"
func main() {
   t := time.After(time.Second * 3)
   fmt.Printf("t type=%T\n", t)
   //阻塞3秒
   fmt.Println("t=", <-t)

基于time.After()特性可以配合select实现计时器

package main
import (
   "fmt"
   "time"
func main() {
   ch1 := make(chan int, 1)
   ch1 <- 1
   for {
      select {
      case e1 := <-ch1:
         //如果ch1通道成功读取数据,则执行该case处理语句
         fmt.Printf("1th case is selected. e1=%v\n", e1)
      case <-time.After(time.Second*2):
         fmt.Println("Timed out")

select语句阻塞等待最先返回数据的channel`,如ch1通道成功读取数据,则先输出1th case is selected. e1=1,之后每隔2s输出 Timed out。

period int64 f func(interface{}, uintptr) // NOTE: must not be closure arg interface{} seq uintptr

在使用定时器Timer的时候都是通过 NewTimerAfterFunc 函数来获取。
先来看一下NewTimer的实现:

func NewTimer(d Duration) *Timer {
    c := make(chan Time, 1)
    t := &Timer{
        C: c,
        r: runtimeTimer{
            when: when(d), //表示达到时间段d时候调用f
            f:    sendTime,  // f表示一个函数调用,这里的sendTime表示d时间到达时向Timer.C发送当前的时间
            arg:  c,  // arg表示在调用f的时候把参数arg传递给f,c就是用来接受sendTime发送时间的
    startTimer(&t.r)
    return t

定时器的具体实现逻辑,都在 runtime 中的 time.go 中,它的实现,没有采用经典 Unix 间隔定时器 setitimer 系统调用,也没有 采用 POSIX间隔式定时器(相关系统调用:timer_createtimer_settimetimer_delete),而是通过四叉树堆(heep)实现的(runtimeTimer 结构中的i字段,表示在堆中的索引)。通过构建一个最小堆,保证最快拿到到期了的定时器执行。定时器的执行,在专门的 goroutine 中进行的:go timerproc()。有兴趣的同学,可以阅读 runtime/time.go 的源码。

func After(d Duration) <-chan Time { return NewTimer(d).C }

根据源码可以看到After直接是返回了Timerchannel,这种就可以做超时处理。
比如我们有这样一个需求:我们写了一个爬虫,爬虫在HTTP GET 一个网页的时候可能因为网络的原因就一只等待着,这时候就需要做超时处理,比如只请求五秒,五秒以后直接丢掉不请求这个网页了,或者重新发起请求。

go Get("http://baidu.com/")
func Get(url string) {
    response := make(chan string)
    response = http.Request(url)
    select {
    case html :=<- response:
        println(html)
    case <-time.After(time.Second * 5):
        println("超时处理")

可以从代码中体现出来,如果五秒到了,网页的请求还没有下来就是执行超时处理,因为Timer的内部会是帮你在你设置的时间长度后自动向Timer.C中写入当前时间。

其实也可以写成这样:

func Get(url string) {
    response := make(chan string)
    response = http.Request(url)
    timeOut := time.NewTimer(time.Second * 3)
    select {
    case html :=<- response:
        println(html)
    case <-timeOut.C:
        println("超时处理")
  • func (t *Timer) Reset(d Duration) bool //强制的修改timer中规定的时间,Reset会先调用 stopTimer 再调用 startTimer类似于废弃之前的定时器,重新启动一个定时器,ResetTimer还未触发时返回true;触发了或Stop了,返回false
  • func (t *Timer) Stop() bool // 如果定时器还未触发,Stop 会将其移除,并返回 true;否则返回 false;后续再对该 Timer 调用 Stop,直接返回 false
  • 比如我写了了一个简单的事例:每两秒给你的女票发送一个"I Love You!"

    // 其中协程之间的控制做的不太好,可以使用channel或者golang中的context来控制
    package main
    import (
        "time"
        "fmt"
    func main() {
        go Love() // 起一个协程去执行定时任务
        stop := 0
        for {
            fmt.Scan(&stop)
            if stop == 1{
                break
    func Love() {
        timer := time.NewTimer(2 * time.Second)  // 新建一个Timer
        for {
            select {
            case <-timer.C:
                fmt.Println("I Love You!")
                timer.Reset(2 * time.Second)  // 上一个when执行完毕重新设置
        return
    
  • func AfterFunc(d Duration, f func()) *Timer // 在时间d后自动执行函数f
  • func main() {
        f := func(){fmt.Println("I Love You!")}
        time.AfterFunc(time.Second*2, f)
        time.Sleep(time.Second * 4)
    

    自动在2秒后打印 "I Love You!"

    time.Ticker

    如果学会了Timer那么Ticker就很简单了,TimerTicker结构体的结构是一样的,举一反三,其实Ticker就是一个重复版本的Timer,它会重复的在时间d后向Ticker中写数据

  • func NewTicker(d Duration) *Ticker // 新建一个Ticker
  • func (t *Ticker) Stop() // 停止Ticker
  • func Tick(d Duration) <-chan Time // Ticker.C 的封装
  • TickerTimer 类似,区别是:Ticker 中的runtimeTimer字段的 period 字段会赋值为 NewTicker(d Duration) 中的d,表示每间隔d纳秒,定时器就会触发一次。

    除非程序终止前定时器一直需要触发,否则,不需要时应该调用 Ticker.Stop 来释放相关资源。

    如果程序终止前需要定时器一直触发,可以使用更简单方便的 time.Tick 函数,因为 Ticker 实例隐藏起来了,因此,该函数启动的定时器无法停止。

    那么这样我们就可以把发"I Love You!"的例子写得简单一些。

    func main() {
        //定义一个ticker
        ticker := time.NewTicker(time.Millisecond * 500)
        //Ticker触发
        go func() {
            for t := range ticker.C {
                fmt.Println(t)
                fmt.Println("I Love You!")
        }()
        time.Sleep(time.Second * 18)
        //停止ticker
        ticker.Stop()
    

    定时器的实际应用

    在实际开发中,定时器用的较多的会是 Timer,如模拟超时,而需要类似 Tiker 的功能时,可以使用实现了 cron spec 的库 cron

     首先time.Timer和 time.NewTicker属于定时器,二者的区别在于

    timer : 到固定时间后会执行一次,请注意是一次,而不是多次。但是可以通过reset来实现每隔固定时间段执行

    ticker : 每隔固定时间都会触发,多次执行. 具体请查看下面示例1

    time.After : 用于实时超时控制,常见主要和select channel结合使用.查看代码示例2

    没有关闭定时器的执行。定时器未关闭!!!!大家会想到stop ,使用stop注意是在协程内还是携程外,以及使用的场景业务

    协程退出时需要关闭,避免资源l浪费,使用defer ticker.Stop() 

    package main
    import (
        "fmt"
        "time"
    //定时器的stop
    func main() {
        // 协程内的定时器 stop  在协程结束时,关闭默认资源定时器,channel 具体根据业务来看
        go func() {
            ticker := time.NewTicker(5 * time.Second)
            // 此处 可以简化为defer ticker.Stop()
            defer func() {
                fmt.Println("stop")
                ticker.Stop()
           select {
           case <- ticker.C:
                fmt.Println("ticker..." )
        // 停止ticker
        stopChan := make(chan bool)
        ticker := time.NewTicker(5 * time.Second)
        go func(ticker *time.Ticker) {
            defer func() {
                ticker.Stop()
                fmt.Println("Ticker2 stop")
            for {
                select {
                case s := <-ticker.C:
                    fmt.Println("Ticker2....",s)
                case stop := <-stopChan:
                    if stop {
                        fmt.Println("Stop")
                        return
        }(ticker)
        // 此处的stop 并不会结束上面协程,也不会打印出 Ticker2 stop  只能借助stopChan,让协程结束时关闭ticker或者协程出现panic时执行defer
        //ticker.Stop()
        stopChan <- true
        close(stopChan)
        time.Sleep(time.Second * 10)
        fmt.Println("main end")
    

    timer正确的stop 问题

     使用 Golang Timer 的正确方式
    https://www.codercto.com/a/34856.html

    一、标准 Timer 的问题

    以下讨论只针对由 NewTimer 创建的 Timer,因为这种 Timer 会使用 channel 来传递到期事件,而正确操作 channel 并非易事。

    Timer.Stop

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

    if !t.Stop