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)
}

相关文章
|
Go 调度
golang 系列:定时器 timer
在 Go 里有很多种定时器的使用方法,像常规的 Timer、Ticker 对象,以及经常会看到的 time.After(d Duration) 和 time.Sleep(d Duration) 方法,今天将会介绍它们的使用方法以及会对它们的底层源码进行分析,以便于在更好的场景中使用定时器。
371 0
|
2月前
|
监控 算法 Go
Golang深入浅出之-Go语言中的服务熔断、降级与限流策略
【5月更文挑战第4天】本文探讨了分布式系统中保障稳定性的重要策略:服务熔断、降级和限流。服务熔断通过快速失败和暂停故障服务调用来保护系统;服务降级在压力大时提供有限功能以保持整体可用性;限流控制访问频率,防止过载。文中列举了常见问题、解决方案,并提供了Go语言实现示例。合理应用这些策略能增强系统韧性和可用性。
137 0
|
2月前
|
前端开发 Go
Golang深入浅出之-Go语言中的异步编程与Future/Promise模式
【5月更文挑战第3天】Go语言通过goroutines和channels实现异步编程,虽无内置Future/Promise,但可借助其特性模拟。本文探讨了如何使用channel实现Future模式,提供了异步获取URL内容长度的示例,并警示了Channel泄漏、错误处理和并发控制等常见问题。为避免这些问题,建议显式关闭channel、使用context.Context、并发控制机制及有效传播错误。理解并应用这些技巧能提升Go语言异步编程的效率和健壮性。
82 5
Golang深入浅出之-Go语言中的异步编程与Future/Promise模式
|
2月前
|
Prometheus 监控 Cloud Native
Golang深入浅出之-Go语言中的分布式追踪与监控系统集成
【5月更文挑战第4天】本文探讨了Go语言中分布式追踪与监控的重要性,包括追踪的三个核心组件和监控系统集成。常见问题有追踪数据丢失、性能开销和监控指标不当。解决策略涉及使用OpenTracing或OpenTelemetry协议、采样策略以及聚焦关键指标。文中提供了OpenTelemetry和Prometheus的Go代码示例,强调全面可观测性对微服务架构的意义,并提示选择合适工具和策略以确保系统稳定高效。
175 5
|
2月前
|
监控 负载均衡 算法
Golang深入浅出之-Go语言中的协程池设计与实现
【5月更文挑战第3天】本文探讨了Go语言中的协程池设计,用于管理goroutine并优化并发性能。协程池通过限制同时运行的goroutine数量防止资源耗尽,包括任务队列和工作协程两部分。基本实现思路涉及使用channel作为任务队列,固定数量的工作协程处理任务。文章还列举了一个简单的协程池实现示例,并讨论了常见问题如任务队列溢出、协程泄露和任务调度不均,提出了解决方案。通过合理设置缓冲区大小、确保资源释放、优化任务调度以及监控与调试,可以避免这些问题,提升系统性能和稳定性。
72 6
|
2月前
|
负载均衡 算法 Go
Golang深入浅出之-Go语言中的服务注册与发现机制
【5月更文挑战第4天】本文探讨了Go语言中服务注册与发现的关键原理和实践,包括服务注册、心跳机制、一致性问题和负载均衡策略。示例代码演示了使用Consul进行服务注册和客户端发现服务的实现。在实际应用中,需要解决心跳失效、注册信息一致性和服务负载均衡等问题,以确保微服务架构的稳定性和效率。
41 3
|
2月前
|
安全 Go
Golang深入浅出之-Go语言中的并发安全队列:实现与应用
【5月更文挑战第3天】本文探讨了Go语言中的并发安全队列,它是构建高性能并发系统的基础。文章介绍了两种实现方法:1) 使用`sync.Mutex`保护的简单队列,通过加锁解锁确保数据一致性;2) 使用通道(Channel)实现无锁队列,天生并发安全。同时,文中列举了并发编程中常见的死锁、数据竞争和通道阻塞问题,并给出了避免这些问题的策略,如明确锁边界、使用带缓冲通道、优雅处理关闭以及利用Go标准库。
47 5
|
2月前
|
分布式计算 Java Go
Golang深入浅出之-Go语言中的分布式计算框架Apache Beam
【5月更文挑战第6天】Apache Beam是一个统一的编程模型,适用于批处理和流处理,主要支持Java和Python,但也提供实验性的Go SDK。Go SDK的基本概念包括`PTransform`、`PCollection`和`Pipeline`。在使用中,需注意类型转换、窗口和触发器配置、资源管理和错误处理。尽管Go SDK文档有限,生态系统尚不成熟,且性能可能不高,但它仍为分布式计算提供了可移植的解决方案。通过理解和掌握Beam模型,开发者能编写高效的数据处理程序。
170 1
|
2月前
|
缓存 测试技术 持续交付
Golang深入浅出之-Go语言中的持续集成与持续部署(CI/CD)
【5月更文挑战第5天】本文介绍了Go语言项目中的CI/CD实践,包括持续集成与持续部署的基础知识,常见问题及解决策略。测试覆盖不足、版本不一致和构建时间过长是主要问题,可通过全面测试、统一依赖管理和利用缓存优化。文中还提供了使用GitHub Actions进行自动化测试和部署的示例,强调了持续优化CI/CD流程以适应项目需求的重要性。
76 1
|
2月前
|
Kubernetes Cloud Native Go
Golang深入浅出之-Go语言中的云原生开发:Kubernetes与Docker
【5月更文挑战第5天】本文探讨了Go语言在云原生开发中的应用,特别是在Kubernetes和Docker中的使用。Docker利用Go语言的性能和跨平台能力编写Dockerfile和构建镜像。Kubernetes,主要由Go语言编写,提供了方便的客户端库与集群交互。文章列举了Dockerfile编写、Kubernetes资源定义和服务发现的常见问题及解决方案,并给出了Go语言构建Docker镜像和与Kubernetes交互的代码示例。通过掌握这些技巧,开发者能更高效地进行云原生应用开发。
89 1