golang timer定时器


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
            }
        }
    }
}(t)
time.Sleep(10 * time.Second)
ch <- true
close(ch)
time.Sleep(1 * time.Second)

定时器(NewTicker)

package main

import (
	"fmt"
	"time"
)

func main() {
		t := time.NewTicker(time.Second*2)
		defer t.Stop()
		for {
			<- t.C
			fmt.Println("Ticker running...")
		}		
}

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。


time.Timer

结构

首先我们看Timer的结构定义:

type runtimeTimer struct {
    tb uintptr
    i  int

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

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

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中写入当前时间。

其实也可以写成这样:

// 其中协程之间的控制做的不太好,可以使用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() {
    //定义一个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() {
	<-t.C
}

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

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

但是因为 channel 的发送和接收发生在不同的 goroutine,所以 存在竞争条件 (race condition),最终可能导致 channel 中的事件未被排掉。

以下就是一种有问题的场景,按时间先后顺序发生:

select...case <-t.C

Timer.Reset

按照 Timer.Reset 文档 的说法,要正确地 Reset Timer,首先需要正确地 Stop Timer。因此 Reset 的问题跟 Stop 基本相同。

二、使用 Timer 的正确方式

参考 Russ Cox 的回复( 这里 和 这里 ),目前 Timer 唯一合理的使用方式是:

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

如果每次使用 Timer 都要按照上述方式来处理,无疑是一件很费神的事。为此,我专门写了一个 Go 库 goodtimer 来解决标准 Timer 的问题。懒是一种美德 :-)



本文链接:https://www.codercto.com/a/34856.html

https://www.jianshu.com/p/372f714c2cf3

https://studygolang.com/articles/9289

链接:https://www.jianshu.com/p/2b4686b8de4a

http://russellluo.com/2018/09/the-correct-way-to-use-timer-in-golang.html

参考;

https://blog.csdn.net/guyan0319/article/details/90450958