Go里供应了两种定时器:Timer(到达指定时间触发且只触发一次)和 Ticker(间隔特定时间触发)。
Timer和Ticker的实现险些一样,Ticker相对繁芜一些,这里紧张讲述一下Ticker是如何实现的。
让我们先来看一下如何利用Ticker
//创建Ticker,设置多永劫光触发一次ticker := time.NewTicker(time.Second 10)go func() { for range ticker.C { //遍历ticker.C,如果有值,则会实行do someting,否则壅塞 //do someting }}()
代码很简洁,给开拓者供应了巨大的便利。那GoLang是如何实现这个功能的呢?
事理NewTickertime/tick.go的NewTicker函数:
调用NewTicker可以天生Ticker,关于这个函数有四点须要解释
NewTicker紧张浸染之一是初始化NewTicker中的韶光因此纳秒为单位的,when返回的从当前韶光+d的纳秒值,d必须为正值Ticker构造体中包含channel,sendTime是个function,逻辑为用select等待c被赋值神秘的startTimer函数,揭示channel、sendTime是如何关联的// NewTicker returns a new Ticker containing a channel that will send the// time with a period specified by the duration argument.// It adjusts the intervals or drops ticks to make up for slow receivers.// The duration d must be greater than zero; if not, NewTicker will panic.// Stop the ticker to release associated resources.func NewTicker(d Duration) Ticker { if d <= 0 { panic(errors.New("non-positive interval for NewTicker")) } // Give the channel a 1-element time buffer. // If the client falls behind while reading, we drop ticks // on the floor until the client catches up. c := make(chan Time, 1) t := &Ticker{ C: c, r: runtimeTimer{ when: when(d), period: int64(d), f: sendTime, arg: c, }, } startTimer(&t.r) return t}
time/tick.go的Ticker数据构造
// A Ticker holds a channel that delivers `ticks' of a clock// at intervals.type Ticker struct { C <-chan Time // The channel on which the ticks are delivered. r runtimeTimer}
time/sleep.go的runtimeTimer
// Interface to timers implemented in package runtime.// Must be in sync with ../runtime/time.go:/^type timertype runtimeTimer struct { tb uintptr i int when int64 period int64 f func(interface{}, uintptr) // NOTE: must not be closure arg interface{} seq uintptr}
time/sleep.go的sendTime
func sendTime(c interface{}, seq uintptr) { // Non-blocking send of time on c. // Used in NewTimer, it cannot block anyway (buffer). // Used in NewTicker, dropping sends on the floor is // the desired behavior when the reader gets behind, // because the sends are periodic. select { case c.(chan Time) <- Now(): default: }}
time/sleep.go的startTimer
func startTimer(runtimeTimer)func stopTimer(runtimeTimer) bool
startTimer
看完上面的代码,大家内心是不是能够猜出是怎么实现的?
有一个机制担保韶光到了时,sendTime被调用,此时channel会被赋值,调用ticker.C的位置解除壅塞,实行指定的逻辑。
让我们看一下GoLang是不是这样实现的。
追踪代码的时候我们创造在time包里的startTimer,只是一个声明,那真正的实现在哪里?
runtime/time.go的startTimer
此处利用go的隐蔽技能go:linkname勾引编译器将当前(私有)方法或者变量在编译时链接到指定的位置的方法或者变量。其余timer和runtimeTimer的构造是同等的,以是程序运行正常。
//startTimer将new的timer工具加入timer的堆数据构造中//startTimer adds t to the timer heap.//go:linkname startTimer time.startTimerfunc startTimer(t timer) { if raceenabled { racerelease(unsafe.Pointer(t)) } addtimer(t)}
runtime/time.go的addtimer
func addtimer(t timer) { tb := t.assignBucket() lock(&tb.lock) ok := tb.addtimerLocked(t) unlock(&tb.lock) if !ok { badTimer() }}
runtime/time.go的addtimerLocked
// Add a timer to the heap and start or kick timerproc if the new timer is// earlier than any of the others.// Timers are locked.// Returns whether all is well: false if the data structure is corrupt// due to user-level races.func (tb timersBucket) addtimerLocked(t timer) bool { // when must never be negative; otherwise timerproc will overflow // during its delta calculation and never expire other runtime timers. if t.when < 0 { t.when = 1<<63 - 1 } t.i = len(tb.t) tb.t = append(tb.t, t) if !siftupTimer(tb.t, t.i) { return false } if t.i == 0 { // siftup moved to top: new earliest deadline. if tb.sleeping && tb.sleepUntil > t.when { tb.sleeping = false notewakeup(&tb.waitnote) } if tb.rescheduling { tb.rescheduling = false goready(tb.gp, 0) } if !tb.created { tb.created = true go timerproc(tb) } } return true}
runtime/time.go的timerproc
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 { //无timer的情形 delta = -1 break } t := tb.t[0] //拿到堆顶的timer delta = t.when - now if delta > 0 { // 所有timer的韶光都没有到期 break } if t.period > 0 { // t[0] 是ticker类型,调度其到期韶光并调度timer堆构造 // leave in heap but adjust next time to fire t.when += t.period (1 + -delta/t.period) siftdownTimer(tb.t, 0) } else { //Timer类型的定时器是单次的,以是这里须要将其从堆里面删除 // remove from heap 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 { siftdownTimer(tb.t, 0) } t.i = -1 // mark as removed } f := t.f arg := t.arg seq := t.seq unlock(&tb.lock) if raceenabled { raceacquire(unsafe.Pointer(t)) } f(arg, seq) //sendTimer被调用的位置 --------------------------------------- lock(&tb.lock) } if delta < 0 || faketime > 0 { // No timers left - put goroutine to sleep. tb.rescheduling = true goparkunlock(&tb.lock, "timer goroutine (idle)", traceEvGoBlock, 1) continue } // At least one timer pending. Sleep until then. tb.sleeping = true tb.sleepUntil = now + delta noteclear(&tb.waitnote) unlock(&tb.lock) notetsleepg(&tb.waitnote, delta) }}
追踪了一圈,终极追踪到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一下,伪代码如下:
func cronMock() { for { //从堆中获取韶光最近的定时器 t := getNearestTime() //如果韶光还没到,则continue t.delta > 0 { continue }else{ //韶光到了,将当前的定时器再加一个钟 t.when += t.duration //将堆重新排序 siftdownTimer() //实行当前定时器指定的函数,即sendTimer t.sendTimer() } }}
资料golang进阶(八)——隐蔽技能go:linkname https://blog.csdn.net/lastsweetop/article/details/78830772从99.9%CPU浅谈Golang的定时器实现事理https://www.jianshu.com/p/c9e8aaa13415末了
大家如果喜好我的文章,可以关注我的公众年夜众号(程序员麻辣烫)
我的个人博客为:https://shidawuhen.github.io/
往期文章回顾:
技能
HTTPS连接过程限流实现2秒杀系统分布式系统与同等性协议微做事之做事框架和注册中央Beego框架利用浅谈微做事TCP性能优化限流实现1Redis实现分布式锁Golang源码BUG追查事务原子性、同等性、持久性的实现事理CDN要求过程详解常用缓存技巧如何高效对接第三方支付Gin框架简洁版InnoDB锁与事务简析算法总结读书条记
敏捷革命如何磨炼自己的影象力大略的逻辑学-读后感热风-读后感论语-读后感孙子兵法-读后感思考
项目流程管理对项目管理的一些意见对产品经理的一些思考关于程序员职业发展的思考关于代码review的思考Markdown编辑器推举-typora