Go: 定时器的生命周期

double12gzh · · 1621 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200704-go-timers-life-cycle/图0.png) > 本篇文章基于 Go `1.14` `定时器` 对于在将来的某个时刻执行代码时非常有用。Go 内部在管理创建的定时器的同时,也会对其执行进行规划。后者可能有点棘手,因为 Go 调度器是一个协作式(`cooperative`)调度器,这意味着一个 goroutine 必须自己停止(阻塞在 `channel` 上,系统调用, 等等)或由调度器在某个调度点暂停。 > 如果想要获取更多关于优先权的信息,我建议你阅读我的文章:[Go:Goroutine 与抢占机制](https://studygolang.com/articles/28972)。 ## 生命周期 下面是一个关于 ` 定时器 ` 的最简单的示例: ```go package main import ( "os" "os/signal" "syscall" "time" ) func main() { sigs := make(chan os.Signal, 1) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) time.AfterFunc(time.Second, func() { println("done") }) <-sigs } ``` 当一个定时器被创建时,它会被保存在与当前 P 关联的定时器的内部列表中,上面的代码可以用下图来表示: ![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200704-go-timers-life-cycle/图1.png) > 如果想要获取更多关于 GMP 模型的内容,建议您可以参考一理我的这篇文章: [Go:协程,操作系统线程和 CPU 管理](https://studygolang.com/articles/25292)。 如图所示,一旦定时器被创建,它就会注册一个内部回调,该回调将用关键字 `go` 调用用户回调,将其转换为 goroutine。 然后,定时器将由调度器进行管理。在每一轮调度中,它都会检查定时器是否准备好运行,如果准备好了,就准备运行。事实上,由于 Go 调度器本身并不运行任何代码,运行定时器的回调会将其 goroutine 加到本地队列中。然后,当调度器在队列中选中它时,goroutine 就会运行。如下图所示: ![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200704-go-timers-life-cycle/图2.png) 根据本地队列的大小,定时器的运行可能会有一些小的延迟。事实上,由于 Go 1.14 中的 `异步抢占`,goroutine 在运行 `10ms` 后就会被抢占,减少了延迟的概率。 ## 时延 为了理解 ` 时延 ` 的可能性,我们来分析一个**从同一个 goroutine 中创建大量定时器的情况**。由于定时器是与当前 P 相连的,所以一个被占用的 P 将无法运行其定时器。这里有一个程序,它创建了数百个定时器,并在其余时间内保持忙碌状态: ```go package main import ( "os" "os/signal" "sync/atomic" "syscall" "time" ) func main() { sigs := make(chan os.Signal, 1) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) var num int64 = 0 for i := 0; i < 1e3; i++ { time.AfterFunc(time.Second, func() { atomic.AddInt64(&num, 1) }) } // 耗时超过 1s t := 0 for i := 0; i < 1e10; i++ { t++ } _ = t <-sigs println(num, "timers created,", t, "iterations done") } ``` 通过下图的 `tracing`,我们可以清楚的看到 goroutine 占用处理器的情况: ![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200704-go-timers-life-cycle/图3.png) 图中的每一个区块表示,由于异步抢占,运行中的 goroutine 的被分成了大量的区块。 > 更多关于异步抢占的内容,请参考我的这篇文章: [Go: 异步抢占](https://studygolang.com/articles/28460) 在这些块中,有一个空间看起来比其他的大。让我们把它放大看一下: ![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200704-go-timers-life-cycle/图4.png) 这个间隔发生在定时器必须运行的时候。此时,当前的 goroutine 已经被抢占,并被 Go 调度器所取代。正如图中高亮部分所示, 调度器将定时器转换为可执行的 goroutine。 然而,当前线程的 Go 调度器并不是唯一一个运行定时器的调度器。Go 实现了一个定时器 `窃取策略`,以确保当前线程相当繁忙时,定时器可以由另一个 `P` 运行。由于异步抢占,这种情况不太可能发生,但在我们的例子中,由于定时器的数量非常多,这种情况还是发生了。如图所示: ![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200704-go-timers-life-cycle/图5.png) 如果我们不考虑定时器 `窃取策略`,下图展示了将会发生的事情: ![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200704-go-timers-life-cycle/图6.png) 所有持有定时器的 goroutine 都被添加到本地队列中。然后,基于 `P` 之间的 `work-stealing` 策略对其重新进行调度分发。 > 更多关于 `work-stealing` 相关资料,请参考我的文章:关于 Go 中工作偷窃的更多信息,我建议你阅读我的文章:[Go 调度器的任务窃取](https://studygolang.com/articles/27146) 综上所述,由于异步抢占和 `work-stealing` 机制,导致延迟发生的可能性很小。

via: https://medium.com/a-journey-with-go/go-timers-life-cycle-403f3580093a

作者:Vincent Blanchon  译者:double12gzh  校对:polaris1119

本文由 GCTT 原创编译,Go语言中文网 荣誉推出


有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1621 次点击  ∙  1 赞  
加入收藏 微博
被以下专栏收入,发现更多相似内容
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传