在 Go 里有不少种定时器的使用方法,像常规的 Timer、Ticker 对象,以及常常会看到的 time.After(d Duration) 和 time.Sleep(d Duration) 方法,今天将会介绍它们的使用方法以及会对它们的底层源码进行分析,以便于在更好的场景中使用定时器。函数
咱们先来看看 Timer 对象 以及 time.After 方法,它们都有点偏一次使用的特性。对于 Timer 来讲,使用完后还能够再次启用它,只须要调用它的 Reset 方法。源码分析
// Timer 例子 func main() { myTimer := time.NewTimer(time.Second * 5) // 启动定时器 for { select { case <-myTimer.C: dosomething() myTimer.Reset(time.Second * 5) // 每次使用完后须要人为重置下 } } // 再也不使用了,结束它 myTimer.Stop() }
// time.After 例子 func main() { timeChannel := time.After(10 * time.Second) select { case <-timeChannel: doSomething() } }
从上面能够看出来 Timer 容许再次被启用,而 time.After 返回的是一个 channel,将不可复用。性能
并且须要注意的是 time.After 本质上是建立了一个新的 Timer 结构体,只不过暴露出去的是结构体里的 channel 字段而已。ui
所以若是在 for{...}
里循环使用了 time.After,将会不断的建立 Timer。以下的使用方法就会带来性能问题:spa
// 错误的案例 !!! func main() { for { // for 里的 time.After 将会不断的建立 Timer 对象 select { case <-time.After(10 * time.Second): doSomething() } } }
看完了有着 “一次特性” 的定时器,接下来咱们来看看按必定时间间隔重复执行任务的定时器:code
func main() { ticker := time.NewTicker(3 * time.Second) for { <-ticker.C doSomething() } ticker.Stop() }
这里的 Ticker 跟 Timer 的不一样之处,就在于 Ticker 时间达到后不须要人为调用 Reset 方法,会自动续期。对象
除了上面的定时器外,Go 里的 time.Sleep 也起到了相似一次性使用的定时功能。只不过 time.Sleep 使用了系统调用。而像上面的定时器更多的是靠 Go 的调度行为来实现。排序
当咱们经过 NewTimer、NewTicker 等方法建立定时器时,返回的是一个 Timer 对象。这个对象里有一个 runtimeTimer 字段的结构体,它在最后会被编译成 src/runtime/time.go 里的 timer 结构体。rem
而这个 timer 结构体就是真正有着定时处理逻辑的结构体。get
一开始,timer 会被分配到一个全局的 timersBucket 时间桶。每当有 timer 被建立出来时,就会被分配到对应的时间桶里了。
为了避免让全部的 timer 都集中到一个时间桶里,Go 会建立 64 个这样的时间桶,而后根据 当前 timer 所在的 Goroutine 的 P 的 id 去哈希到某个桶上:
// assignBucket 将建立好的 timer 关联到某个桶上 func (t *timer) assignBucket() *timersBucket { id := uint8(getg().m.p.ptr().id) % timersLen t.tb = &timers[id].timersBucket return t.tb }
接着 timersBucket 时间桶将会对这些 timer 进行一个最小堆的维护,每次会挑选出时间最快要达到的 timer。
若是挑选出来的 timer 时间还没到,那就会进行 sleep 休眠。
若是 timer 的时间到了,则执行 timer 上的函数,而且往 timer 的 channel 字段发送数据,以此来通知 timer 所在的 goroutine。
上面说起了下定时器的原理,如今咱们来好好看一下定时器 timer 的源码。
首先,定时器建立时,会调用 startTimer 方法:
func startTimer(t *timer) { if raceenabled { racerelease(unsafe.Pointer(t)) } // 1.开始把当前的 timer 添加到 时间桶里 addtimer(t) }
而 addtimer 也就是咱们刚刚所说的分配到某个桶的动做:
func addtimer(t *timer) { tb := t.assignBucket() // 分配到某个时间桶里 lock(&tb.lock) ok := tb.addtimerLocked(t) // 2.添加完后,时间桶执行堆排序,挑选最近的 timer 去执行 unlock(&tb.lock) if !ok { badTimer() } }
addtimerLocked 里包含了最终的时间处理函数: timerproc,重点分析下:
// 当有新的 timer 添加进来时会触发一次 // 当休眠到最近的一次时间到来后,也会触发一次 func timerproc(tb *timersBucket) { tb.gp = getg() for { lock(&tb.lock) tb.sleeping = false now := nanotime() delta := int64(-1) for { if len(tb.t) == 0 { delta = -1 break } t := tb.t[0] delta = t.when - now if delta > 0 { // 定时器的时间还没到 break } ok := true if t.period > 0 { // 此处 period > 0,表示是 ticker 类型的定时器, // 重置下次调用的时间,帮 ticker 自动续期 t.when += t.period * (1 + -delta/t.period) if !siftdownTimer(tb.t, 0) { ok = false } } else { // “一次性” 定时器,而且时间到了,须要先移除掉,再进行后面的动做 last := len(tb.t) - 1 if last > 0 { tb.t[0] = tb.t[last] tb.t[0].i = 0 } tb.t[last] = nil tb.t = tb.t[:last] if last > 0 { if !siftdownTimer(tb.t, 0) { ok = false } } t.i = -1 // 标记已清除 } // 执行到这里表示定时器的时间到了,须要执行对应的函数。 // 这个函数也就是 sendTime,它会往 timer 的 channel 发送数据, // 以通知对应的 goroutine f := t.f arg := t.arg seq := t.seq unlock(&tb.lock) if !ok { badTimer() } if raceenabled { raceacquire(unsafe.Pointer(t)) } f(arg, seq) lock(&tb.lock) } if delta < 0 || faketime > 0 { // 没有定时器须要执行任务,采用 gopark 休眠 // No timers left - put goroutine to sleep. tb.rescheduling = true goparkunlock(&tb.lock, waitReasonTimerGoroutineIdle, traceEvGoBlock, 1) continue } // 有 timer 但它的时间还没到,所以采用 notetsleepg 休眠 tb.sleeping = true tb.sleepUntil = now + delta noteclear(&tb.waitnote) unlock(&tb.lock) notetsleepg(&tb.waitnote, delta) } }
在上面的代码中,发现当时间桶里已经没有定时器的时候,goroutine 会调用 gopark 去休眠,直到又有新的 timer 添加到时间桶,才从新唤起执行定时器的循环代码。
另外,当堆排序挑选出来的定时器时间还没到的话,则会调用 notetsleepg 来休眠,等到休眠时间达到后从新被唤起。
Go 的定时器采用了堆排序来挑选最近的 timer,而且会往 timer 的 channel 字段发送数据,以便通知对应的 goroutine 继续往下执行。
这就是定时器的基础原理了,其余流程也只是休眠唤起的执行罢了,但愿此篇能帮助到你们对 Go 定时器的理解!!!
感兴趣的朋友能够搜一搜公众号「 阅新技术 」,关注更多的推送文章。
能够的话,就顺便点个赞、留个言、分享下,感谢各位支持!
阅新技术,阅读更多的新知识。