简介
工作中经常有定时执行某些代码块的需求,如果是PHP代码,一般写个脚本,然后用Cron实现。
Go里提供了两种定时器:Timer
(到达指定时间触发且只触发一次)和 Ticker
(间隔特定时间触发)。
Timer和Ticker的实现几乎一样,Ticker相对复杂一些,这里主要讲述一下Ticker是如何实现的。
让我们先来看一下如何使用Ticker
1 | //创建Ticker,设置多长时间触发一次 |
代码很简洁,给开发者提供了巨大的便利。那GoLang是如何实现这个功能的呢?
原理
NewTicker
time/tick.go的NewTicker函数:
调用NewTicker可以生成Ticker,关于这个函数有四点需要说明
- NewTicker主要作用之一是初始化
- NewTicker中的时间是以纳秒为单位的,when返回的从当前时间+d的纳秒值,d必须为正值
- Ticker结构体中包含channel,sendTime是个function,逻辑为用select等待c被赋值
- 神秘的startTimer函数,揭示channel、sendTime是如何关联的
1 | // NewTicker returns a new Ticker containing a channel that will send the |
time/tick.go的Ticker数据结构
1 | // A Ticker holds a channel that delivers `ticks' of a clock |
time/sleep.go的runtimeTimer
1 | // Interface to timers implemented in package runtime. |
time/sleep.go的sendTime
1 | func sendTime(c interface{}, seq uintptr) { |
time/sleep.go的startTimer
1 | func startTimer(*runtimeTimer) |
startTimer
看完上面的代码,大家内心是不是能够猜出是怎么实现的?
有一个机制保证时间到了时,sendTime被调用,此时channel会被赋值,调用ticker.C的位置解除阻塞,执行指定的逻辑。
让我们看一下GoLang是不是这样实现的。
追踪代码的时候我们发现在time包里的startTimer,只是一个声明,那真正的实现在哪里?
runtime/time.go的startTimer
此处使用go的隐藏技能go:linkname引导编译器将当前(私有)方法或者变量在编译时链接到指定的位置的方法或者变量。另外timer和runtimeTimer的结构是一致的,所以程序运行正常。
1 | //startTimer将new的timer对象加入timer的堆数据结构中 |
runtime/time.go的addtimer
1 | func addtimer(t *timer) { |
runtime/time.go的addtimerLocked
1 | // Add a timer to the heap and start or kick timerproc if the new timer is |
runtime/time.go的timerproc
1 | func timerproc(tb *timersBucket) { |
追踪了一圈,最终追踪到timerproc,发现了sendTimer被调用位置f(arg, seq) ,而且可以看到将channel c传到了sendTimer中。
上面的这堆代码逻辑是什么意思呢?
- 所有timer统一使用一个最小堆结构去维护,按照timer的when(到期时间)比较大小;
- for循环过程中,如果delta = t.when - now的时间大于0,则break,直到有到时间的timer才进行操作;
- timer处理线程从堆顶开始处理每个timer,对于到期的timer,如果其period>0,则表明该timer 属于Ticker类型,调整其下次到期时间并调整其在堆中的位置,否则从堆中移除该timer;
- 调用该timer的处理函数以及其他相关工作;
总结
读完这篇文章,有没有奇怪的知识又增加了一些的感觉。写这些源码的大神们,对Go的理解很深刻,编码的功能也很深厚。
本质上GoLang用channel和堆实现了定时器功能,让我们来mock一下,伪代码如下:
1 | func cronMock() { |