Go Timer实现原理剖析(轻松掌握Timer实现原理)
前言
本节我们从Timer数据结构入手,结合源码分析Timer的实现原理。
很多人想当然的以为,启动一个Timer意味着启动了一个协程,这个协程会等待Timer到期,然后向Timer的管道中发送当前时间。
实际上,每个Go应用程序都有一个协程专门负责管理所有的Timer,这个协程负责监控Timer是否过期,过期后执行一个预定义的动作,这个动作对于Timer而言就是发送当前时间到管道中。
数据结构
Timer
源码包src/time/sleep.go:Timer
定义了其数据结构:
type Timer struct { C <-chan Time r runtimeTimer }
Timer只有两个成员:
- C: 管道,上层应用跟据此管道接收事件;
- r: runtime定时器,该定时器即系统管理的定时器,对上层应用不可见;
这里应该按照层次来理解Timer数据结构,Timer.C即面向Timer用户的,Timer.r是面向底层的定时器实现。
runtimeTimer
前面我们说过,创建一个Timer实质上是把一个定时任务交给专门的协程进行监控,这个任务的载体便是runtimeTimer
,简单的讲,每创建一个Timer意味着创建一个runtimeTimer变量,然后把它交给系统进行监控。我们通过设置runtimeTimer过期后的行为来达到定时的目的。
源码包src/time/sleep.go:runtimeTimer
定义了其数据结构:
type runtimeTimer struct { tb uintptr // 存储当前定时器的数组地址 i int // 存储当前定时器的数组下标 when int64 // 当前定时器触发时间 period int64 // 当前定时器周期触发间隔 f func(interface{}, uintptr) // 定时器触发时执行的函数 arg interface{} // 定时器触发时执行函数传递的参数一 seq uintptr // 定时器触发时执行函数传递的参数二(该参数只在网络收发场景下使用) }
其成员如下:
- tb: 系统底层存储runtimeTimer的数组地址;
- i: 当前runtimeTimer在tb数组中的下标;
- when: 定时器触发事件的时间;
- period: 定时器周期性触发间隔(对于Timer来说,此值恒为0);
- f: 定时器触发时执行的回调函数,回调函数接收两个参数;
- arg: 定时器触发时执行回调函数的参数一;
- seq: 定时器触发时执行回调函数的参数二(Timer并不使用该参数);
实现原理
一个进程中的多个Timer都由底层的一个协程来管理,为了描述方便我们把这个协程称为系统协程。
我们想在后面的章节中单独介绍系统协程工作机制,本节,我们先简单介绍其工作过程。
系统协程把runtimeTimer存放在数组中,并按照when
字段对所有的runtimeTimer进行堆排序,定时器触发时执行runtimeTimer中的预定义函数f
,即完成了一次定时任务。
创建Timer
我们来看创建Timer的实现,非常简单:
func NewTimer(d Duration) *Timer { c := make(chan Time, 1) // 创建一个管道 t := &Timer{ // 构造Timer数据结构 C: c, // 新创建的管道 r: runtimeTimer{ when: when(d), // 触发时间 f: sendTime, // 触发后执行函数sendTime arg: c, // 触发后执行函数sendTime时附带的参数 }, } startTimer(&t.r) // 此处启动定时器,只是把runtimeTimer放到系统协程的堆中,由系统协程维护 return t }
NewTimer()只是构造了一个Timer,然后把Timer.r通过startTimer()交给系统协程维护。
其中when()方法是计算下一次定时器触发的绝对时间,即当前时间+NewTimer()参数d。
其中sendTime()方法便是定时器触发时的动作:
func sendTime(c interface{}, seq uintptr) { select { case c.(chan Time) <- Now(): default: } }
sendTime接收一个管道作为参数,其主要任务是向管道中写入当前时间。
创建Timer时生成的管道含有一个缓冲区(make(chan Time, 1)
),所以Timer触发时向管道写入时间永远不会阻塞,sendTime写完即退出。
之所以sendTime()使用select并搭配一个空的default分支,是因为后面所要讲的Ticker也复用sendTime(),Ticker触发时也会向管道中写入时间,但无法保证之前的数据已被取走,所以使用select并搭配一个空的default分支,确保sendTime()不会阻塞,Ticker触发时,如果管道中还有值,则本次不再向管道中写入时间,本次触发的事件直接丢弃。
startTimer(&t.r)
的具体实现在runtime包,其主要作用是把runtimeTimer写入到系统协程的数组中,并启动系统协程(如果系统协程还未开始运行的话)。更详细的内容,待后面讲解系统协程时再介绍。
综上,创建一个Timer示意图如下:
停止Timer
停止Timer,只是简单的把Timer从系统协程中移除。函数主要实现如下:
func (t *Timer) Stop() bool { return stopTimer(&t.r) }
stopTimer()即通知系统协程把该Timer移除,即不再监控。系统协程只是移除Timer并不会关闭管道,以避免用户协程读取错误。
系统协程监控Timer是否需要触发,Timer触发后,系统协程会删除该Timer。所以在Stop()执行时有两种情况:
- Timer还未触发,系统协程已经删除该Timer,Stop()返回false;
- Timer已经触发,系统协程还未删除该Timer,Stop()返回true;
综上,停止一个Timer示意图如下:
重置Timer
重置Timer时会先timer先从系统协程中删除,修改新的时间后重新添加到系统协程中。
重置函数主要实现如下所示:
func (t *Timer) Reset(d Duration) bool { w := when(d) active := stopTimer(&t.r) t.r.when = w startTimer(&t.r) return active }
其返回值与Stop()保持一致,即如果Timer成功停止,则返回true,如果Timer已经触发,则返回false。
重置一个Timer示意图如下:
由于新加的Timer时间很可能变化,所以其在系统协程的位置也会发生变化。
需要注意的是,按照官方说明,Reset()应该作用于已经停掉的Timer或者已经触发的Timer,按照这个约定其返回值将总是返回false,之所以仍然保留是为了保持向前兼容,使用老版本Go编写的应用不需要因为Go升级而修改代码。
如果不按照此约定使用Reset(),有可能遇到Reset()和Timer触发同时执行的情况,此时有可能会收到两个事件,从而对应用程序造成一些负面影响,使用时一定要注意。
总结
- NewTimer()创建一个新的Timer交给系统协程监控;
- Stop()通知系统协程删除指定的Timer;
- Reset()通知系统协程删除指定的Timer并再添加一个新的Timer;
赠人玫瑰手留余香,如果觉得不错请给个赞~
本篇文章已归档到GitHub项目,求星~ 点我即达
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
Flutter 实现原理及在马蜂窝的跨平台开发实践
一直以来,跨平台开发都是困扰移动客户端开发的难题。 在马蜂窝旅游 App 很多业务场景里,我们尝试过一些主流的跨平台开发解决方案,比如 WebView 和 React Native,来提升开发效率和用户体验。但这两种方式也带来了新的问题。 比如使用 WebView 跨平台方式,优点确实非常明显。基于 WebView 的框架集成了当下 Web 开发的诸多优势:丰富的控件库、动态化、良好的技术社区、测试自动化等等。但是缺点也同样明显:渲染效率和 JavaScript 的执行能力都比较差,使页面的加载速度和用户体验都不尽如人意。 而使用以 React Native(简称 RN)为代表的框架时,维护又成了大难题。RN 使用类 HTML+JS 的 UI 创建逻辑,生成对应的原生页面,将页面的渲染工作交给了系统,所以渲染效率有很大的优势。但由于 RN 代码是通过 JS 桥接的方式转换为原生的控件,所以受各个系统间的差异影响非常大,虽然可以开发一套代码,但对各个平台的适配却非常的繁琐和麻烦。 为什么是 Flutter 2018 年 12 月初,Google 正式发布了开源跨平台 UI 框架 Flu...
- 下一篇
小程序多端框架全面测评
小程序多端框架到底应该选哪个? 最近前端届多端框架频出,相信很多有代码多端运行需求的开发者都会产生一些疑惑:这些框架都有什么优缺点?到底应该用哪个? 作为 Taro 开发团队一员,笔者想在本文尽量站在一个客观公正的角度去评价各个框架的选型和优劣。但宥于利益相关,本文的观点很可能是带有偏向性的,大家可以带着批判的眼光去看待,权当抛砖引玉。 那么,当我们在讨论多端框架时,我们在谈论什么: 多端 笔者以为,现在流行的多端框架可以大致分为三类: 1. 全包型 这类框架最大的特点就是从底层的渲染引擎、布局引擎,到中层的 DSL,再到上层的框架全部由自己开发,代表框架是 Qt 和 Flutter。这类框架优点非常明显:性能(的上限)高;各平台渲染结果一致。缺点也非常明显:需要完全重新学习 DSL(QML/Dart),以及难以适配中国特色的端:小程序。 这类框架是最原始也是最纯正的的多端开发框架,由于底层到上层每个环节都掌握在自己手里,也能最大可能地去保证开发和跨端体验一致。但它们的框架研发成本巨大,渲染引擎、布局引擎、DSL、上层框架每个部分都需要大量人力开发维护。 2. Web 技术型 这类框架...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- MySQL8.0.19开启GTID主从同步CentOS8
- SpringBoot2全家桶,快速入门学习开发网站教程
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- CentOS8编译安装MySQL8.0.19
- CentOS7,8上快速安装Gitea,搭建Git服务器
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- SpringBoot2配置默认Tomcat设置,开启更多高级功能