![](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语言中文网 首发。也想加入译者行列,为开源做一些自己的贡献么?欢迎加入 GCTT!
翻译工作和译文发表仅用于学习和交流目的,翻译工作遵照 CC-BY-NC-SA 协议规定,如果我们的工作有侵犯到您的权益,请及时联系我们。
欢迎遵照 CC-BY-NC-SA 协议规定 转载,敬请在正文中标注并保留原文/译文链接和作者/译者等信息。
文章仅代表作者的知识和看法,如有不同观点,请楼下排队吐槽
有疑问加站长微信联系(非本文作者))