Golang 定时器详解

简介: Golang 定时器详解

定时器是一种通过设置一项任务,在未来的某个时刻执行该任务的机制。

定时器的种类通常只有两种,一种是只执行一次的延时模式,一种是每隔一段时间执行一次的间隔模式。

在现代编程语言中,定时器几乎是标配。除了设置定时器外,还需要有提供定时器的方法。

比如在 JavaScript 中,提供了 setTimeout、setInterval、clearTimeout 和 clearInterval 四个 API,相比较而言是比较简单的。Go 语言中定时器的 API 就比较完善,所有的 API 都在 time 包中。


实际应用


延迟执行


延迟执行有两种方式,time.After 和 time.Sleep。


time.After


下面是输出 1 之后间隔 1 秒后再输出 2 的例子。


func main() {
  fmt.Println("1")
  timeAfterTrigger := time.After(1 * time.Second)
  <-timeAfterTrigger
  fmt.Println("2")
}

其中 After 的参数 Duration 单位是纳秒。time 包中提供了运算好的几个 int 类型常量。


const (
  Nanosecond  Duration = 1
  Microsecond          = 1000 * Nanosecond
  Millisecond          = 1000 * Microsecond
  Second               = 1000 * Millisecond
  Minute               = 60 * Second
  Hour                 = 60 * Minute
)


time.Sleep


相比较而言,time.Sleep 实现同样的效果用起来就更加简洁了。


func main() {
  fmt.Println("1")
  time.Sleep(1 * time.Second)
  fmt.Println("2")
}

两者的区别在于 time.Sleep 是阻塞当前协程,time.After 基于 channel 实现,可以在不同的协程中传递。

time.Sleep 的底层也是通过 Timer 实现的。


time.AfterFunc


除了 time.After 和 time.Sleep 以外,还有 AfterFunc 函数。作用类似 After,可以在延迟一段时间后触发某个函数。 下面是输出 1 ,然后延迟 1 秒后输出 2 的例子。


func main() {
    fmt.Println("1")
    c := make(chan int, 1)
    time.AfterFunc(1*time.Second, func() {
        fmt.Println("2")
        c <- 1
    })
    <-c
}


定时执行


定时执行又分两种情况,一种是执行 N 次后结束,另一种是程序不终止一直执行。


time.NewTicker


下面是输出 1 之后间每隔 1 秒输出 2,连续输出 5 次后结束的例子。


func main() {
  fmt.Println("1")
  count := 0
  timeTicker := time.NewTicker(1 * time.Second)
  for {
    <-timeTicker.C
    fmt.Println("2")
    count++
    if count >= 5 {
      timeTicker.Stop()
    }
  }
}


time.Tick


下面是每隔 1 秒输出 2,程序不终止一直循环输出的例子。


func main() {
  t := time.Tick(1 * time.Second)
  for {
    <-t
    fmt.Println("每隔 1 秒输出一次")
  }
}


控制定时器


定时器提供了 Stop 方法和 Reset 方法。

Stop 方法的作用是停止定时器,Reset 方法的作用是改变定时器的间隔时间。


time.Stop


Stop 方法的应用在上面的实例中已经展示了。


time.Reset


下面是一个输出 1 之后每隔 1 秒输出 2 ,输出 3 次 2 后改为每隔 2 秒输出 2 的例子。


func main() {
  fmt.Println("1")
  count := 0
  timeTicker := time.NewTicker(1 * time.Second)
  for {
    <-timeTicker.C
    fmt.Println("2")
    count++
    if count >= 3 {
      timeTicker.Reset(2 * time.Second)
    }
  }
}


源码分析


Go 程序中所有的定时器共同使用一个协程来管理,而不是每个定时器启用一个协程。


time.Timer


以上几个 API 都是基于结构体 Timer 实现。


type Timer struct {
  C <-chan Time
  r runtimeTimer
}

C 是单次时间间隔的 channel,每次 Timer 到期,会将当前时间发送给 C。

另一个属性是 r,类型是 runtimeTimer,它是内部结构,不会暴漏给开发者。


type runtimeTimer struct {
  pp       uintptr
  when     int64
  period   int64
  f        func(interface{}, uintptr) // NOTE: must not be closure
  arg      interface{}
  seq      uintptr
  nextwhen int64
  status   uint32
}


time.NewTimer


要理解 runtimeTimer 的作用,要来看创建 Timer 的 NewTimer 函数。


func NewTimer(d Duration) *Timer {
  c := make(chan Time, 1)// 创建缓冲管道
  t := &Timer{
    C: c,// 新创建的管道
    r: runtimeTimer{
      when: when(d),// 触发时间
      f:    sendTime,// 触发后执行的函数
      arg:  c,// 触发后 sendTime 函数执行时的参数
    },
  }
  startTimer(&t.r)// 启动定时器,将创建的 runtimeTimer 放到系统协程的堆中
  return t// 返回定时器
}


time.when


该函数在 sleep.go 中,是一个内部函数,作用是计算出下一次执行的绝对时间。


func when(d Duration) int64 {
  if d <= 0 {
    return runtimeNano()
  }
  t := runtimeNano() + int64(d)
  if t < 0 {
    t = 1<<63 - 1 // math.MaxInt64
  }
  return t
}


time.sendTime


将当前时间放入管道,用于触发定时器。


func sendTime(c interface{}, seq uintptr) {
  select {
  case c.(chan Time) <- Now():
  default:
  }
}

由于创建的管道 C 是缓冲管道,所以不会产生阻塞。sendTime 发送完当前时间后就会退出。

default 空分支的作用是 Ticker 也使用了 sendTime,Ticker 触发时也会向管道发送时间,但无法保证之前的时间已被取走,所以使用 select 搭配 default 保证 sendTime 不会阻塞。

startTimer 在 runtime 包中,作用是把 runtimeTimer 添加到系统协程的数组中,如果系统协程未启动,还会启动系统协程。


time.Stop


stop 简单的调用了 runtime 包中的 stopTimer 方法,系统协程不再监控 Timer,但是 Timer 的管道并不会关闭,因为用户协程还可能读取这个管道。

Timer 如果已经触发,再调用 Stop,会返回 false,表示 Stop 失败。


func (t *Timer) Stop() bool {
  if t.r.f == nil {
    panic("time: Stop called on uninitialized Timer")
  }
  return stopTimer(&t.r)
}


time.Reset


reset 调用了 runtime 包的 modTimer,会把 Timer 删除掉,再修改时间,重新添加到系统协程中。

返回值与 stop 保持一致。


func (t *Ticker) Reset(d Duration) {
  if t.r.f == nil {
    panic("time: Reset called on uninitialized Ticker")
  }
  modTimer(&t.r, when(d), int64(d), t.r.f, t.r.arg, t.r.seq)
}

相关文章
|
3月前
|
Go
Golang的time.NewTimer单次定时器使用案例
这篇文章介绍了Go语言中time包的多种定时器使用案例,包括单次定时器的创建、阻塞程序运行的Sleep函数、重置和停止定时器的方法,以及After和AfterFunc函数的使用。
59 5
Golang的time.NewTimer单次定时器使用案例
|
3月前
|
Go
Golang的time.NewTicker周期性定时器使用案例
这篇文章介绍了Golang中time包的NewTicker函数如何创建周期性定时器,并通过两个示例展示了如何使用定时器以及如何停止定时器。
79 1
|
Go 调度
golang 系列:定时器 timer
在 Go 里有很多种定时器的使用方法,像常规的 Timer、Ticker 对象,以及经常会看到的 time.After(d Duration) 和 time.Sleep(d Duration) 方法,今天将会介绍它们的使用方法以及会对它们的底层源码进行分析,以便于在更好的场景中使用定时器。
405 0
|
3月前
|
Go
Golang语言之管道channel快速入门篇
这篇文章是关于Go语言中管道(channel)的快速入门教程,涵盖了管道的基本使用、有缓冲和无缓冲管道的区别、管道的关闭、遍历、协程和管道的协同工作、单向通道的使用以及select多路复用的详细案例和解释。
140 4
Golang语言之管道channel快速入门篇
|
3月前
|
Go
Golang语言文件操作快速入门篇
这篇文章是关于Go语言文件操作快速入门的教程,涵盖了文件的读取、写入、复制操作以及使用标准库中的ioutil、bufio、os等包进行文件操作的详细案例。
71 4
Golang语言文件操作快速入门篇
|
3月前
|
Go
Golang语言之gRPC程序设计示例
这篇文章是关于Golang语言使用gRPC进行程序设计的详细教程,涵盖了RPC协议的介绍、gRPC环境的搭建、Protocol Buffers的使用、gRPC服务的编写和通信示例。
112 3
Golang语言之gRPC程序设计示例
|
3月前
|
安全 Go
Golang语言goroutine协程并发安全及锁机制
这篇文章是关于Go语言中多协程操作同一数据问题、互斥锁Mutex和读写互斥锁RWMutex的详细介绍及使用案例,涵盖了如何使用这些同步原语来解决并发访问共享资源时的数据安全问题。
100 4
|
3月前
|
Go
Golang语言错误处理机制
这篇文章是关于Golang语言错误处理机制的教程,介绍了使用defer结合recover捕获错误、基于errors.New自定义错误以及使用panic抛出自定义错误的方法。
54 3
|
3月前
|
Go 调度
Golang语言goroutine协程篇
这篇文章是关于Go语言goroutine协程的详细教程,涵盖了并发编程的常见术语、goroutine的创建和调度、使用sync.WaitGroup控制协程退出以及如何通过GOMAXPROCS设置程序并发时占用的CPU逻辑核心数。
72 4
Golang语言goroutine协程篇
|
3月前
|
Prometheus Cloud Native Go
Golang语言之Prometheus的日志模块使用案例
这篇文章是关于如何在Golang语言项目中使用Prometheus的日志模块的案例,包括源代码编写、编译和测试步骤。
71 3
Golang语言之Prometheus的日志模块使用案例