专栏首页后端golang 系列:定时器 timer
原创

golang 系列:定时器 timer

摘要

在 Go 里有很多种定时器的使用方法,像常规的 Timer、Ticker 对象,以及经常会看到的 time.After(d Duration) 和 time.Sleep(d Duration) 方法,今天将会介绍它们的使用方法以及会对它们的底层源码进行分析,以便于在更好的场景中使用定时器。

Go 里的定时器

我们先来看看 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 字段而已。

因此如果在 for{...}里循环使用了 time.After,将会不断的创建 Timer。如下的使用方法就会带来性能问题:

// 错误的案例 !!!
	func main() {
		for { // for 里的 time.After 将会不断的创建 Timer 对象
			select {
				case <-time.After(10 * time.Second):
				doSomething()
			}
		}
	}

看完了有着 “一次特性” 的定时器,接下来我们来看看按一定时间间隔重复执行任务的定时器:

	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 结构体。

而这个 timer 结构体就是真正有着定时处理逻辑的结构体。

一开始,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 定时器的理解!!!


感兴趣的朋友可以搜一搜公众号「 阅新技术 」,关注更多的推送文章。

可以的话,就顺便点个赞、留个言、分享下,感谢各位支持!

阅新技术,阅读更多的新知识。

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 转-- Golang中timer定时器实现原理

    一般我们导入import ("time")包,然后调用time.NewTicker(1 * time.Second) 实现一个定时器: func timer1(...

    李海彬
  • Python 定时器 timer

    Java学习123
  • Linux systemd 定时器 timer

    用来取代 crontab systemd 系列文章请查看:https://www.khs1994.com/tags/systemd/ 要使用定时器必须编写两个...

    康怀帅
  • Java Timer定时器原理

    做项目很多时候会用到定时任务,比如在深夜,流量较小的时候,做一些统计工作。早上定时发送邮件,更新数据库等。这里可以用Java的Timer或线程池实现。Timer...

    技术从心
  • python 线程定时器Timer

    相对前面几篇python线程内容而言,本片内容相对比较简单,定时器 – 顾名思义,必然用于定时任务。

    猿说编程[Python和C]
  • Java 定时器 Timer 的使用.

    一、概念       定时计划任务功能在Java中主要使用的就是Timer对象,它在内部使用多线程的方式进行处理,所以它和多线程技术还是有非常大的关联的。在JD...

    JMCui
  • 32.python 线程定时器Timer

    相对前面几篇python线程内容而言,本片内容相对比较简单,定时器 – 顾名思义,必然用于定时任务。

    猿说编程[Python和C]
  • 性能测试-Jmeter定时器(Timer)

    用法(场景):更真实的模拟用户场景,需要设置等待时间,或是等待上一个请求的时间,才执行,给sampler之间的思考时间;

    用户6367961
  • 定时器Timer(迭代一)测试篇

    挑战者
  • deno深入揭秘及未来展望

    node.js之父Ryan Dahl在一个月前发起了名为deno的项目,项目的初衷是打造一个基于v8引擎的安全的TypeScript运行时,同时实现HTML5...

    欲休
  • Java多线程学习(七)——定时器Timer

    在JDK库中,Timer类主要负责计划任务的功能,也就是在指定的时间开始执行某一个任务。

    小森啦啦啦
  • python通过线程实现定时器timer

    下面介绍以threading模块来实现定时器的方法。  使用前先做一个简单试验: 

    py3study
  • Golang之定时器,recover

    超蛋lhy
  • golang定时器实现

    简单总结一下,个人推荐使用context,因为能够更加方便控制定时器的停止时间,同时还可以在每次执行定时器业务逻辑的时候进行判断是否达到定时器的停止条件,从而停...

    Java架构师必看
  • 如何设计和实现微信公众号关注后48小时内定时给粉丝自动推送发送图文图片或文本消息?

    很多人可能会留意到, 关注了公众号之后,隔一段时间, 公众号会推送消息出来,打开消息后发现这些消息看起来不像人工发送的,应该是设计好的一套关注后的定时推送机制,...

    扫地工程师
  • leaf源码分析(二)----skeleton

    版权声明:本文为作者原创,如需转载请通知本人,并标明出处和作者。擅自转...

    月牙寂道长
  • Go 超时引发大量 fin-wait2

    通过grafana监控面板,发现了几个高频的业务缓存节点出现了大量的fin-wait2,而且fin-wait2状态持续了不短的时间。通过连接的ip地址和抓包数据...

    梦醒人间
  • Flink Timer(定时器)机制及实现详解

    Timer(定时器)是Flink Streaming API提供的用于感知并利用处理时间/事件时间变化的机制。官网上给出的描述如下:

    大数据真好玩
  • Kubernetes 是怎么实现定时任务的

    Kubernetes 的各个组件都有一定的定时任务,比如日志的处理、任务的查询、缓存的使用等。Kubernetes 中的定时任务都是通过 wait 包实现的,比...

    CS实验室

扫码关注云+社区

领取腾讯云代金券