Go Timer实现原理剖析(轻松掌握Timer实现原理)

in 编程
关注公众号【好便宜】( ID:haopianyi222 ),领红包啦~
阿里云,国内最大的云服务商,注册就送数千元优惠券:https://t.cn/AiQe5A0g
腾讯云,良心云,价格优惠: https://t.cn/AieHwwKl
搬瓦工,CN2 GIA 优质线路,搭梯子、海外建站推荐: https://t.cn/AieHwfX9

前言

本节我们从Timer数据结构入手,结合源码分析Timer的实现原理。

很多人想当然的以为,启动一个Timer意味着启动了一个协程,这个协程会等待Timer到期,然后向Timer的管道中发送当前时间。

实际上,每个Go应用程序都有一个协程专门负责管理所有的Timer,这个协程负责监控Timer是否过期,过期后执行一个预定义的动作,这个动作对于Timer而言就是发送当前时间到管道中。

数据结构

Timer

源码包src/time/sleep.go:Timer定义了其数据结构:

type Timer struct {
	C <-chan Time
	r runtimeTimer
}

Timer只有两个成员:

这里应该按照层次来理解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                      // 定时器触发时执行函数传递的参数二(该参数只在网络收发场景下使用)
}

其成员如下:

实现原理

一个进程中的多个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

重置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触发同时执行的情况,此时有可能会收到两个事件,从而对应用程序造成一些负面影响,使用时一定要注意。

总结

赠人玫瑰手留余香,如果觉得不错请给个赞~

本篇文章已归档到GitHub项目,求星~ 点我即达

关注公众号【好便宜】( ID:haopianyi222 ),领红包啦~
阿里云,国内最大的云服务商,注册就送数千元优惠券:https://t.cn/AiQe5A0g
腾讯云,良心云,价格优惠: https://t.cn/AieHwwKl
搬瓦工,CN2 GIA 优质线路,搭梯子、海外建站推荐: https://t.cn/AieHwfX9
扫一扫关注公众号添加购物返利助手,领红包
Comments are closed.

推荐使用阿里云服务器

超多优惠券

服务器最低一折,一年不到100!

朕已阅去看看