前言

Go提供了两种定时器, 即 一次性定时器周期定时器

  • 一次性定时器:定时器只计时一次,结束便停止
  • 周期定时器:定时器周期性进行计时

本篇将快速介绍这两种定时器的基本用法,重点介绍其内部实现原理,最后再给出一个案例揭示使用定时器的风险。

Timer 定时器

简介

Timer实际上是一种单一事件的定时器,即经过指定的时间后触发一个事件,这个事件通过其本身提供的channel进行通知。之所以叫单一事件,是因为Timer只执行一次就结束,这也是Timer与Ticker的最重要的区别之一。

通过src/time.sleep.go:Timer定义了 Timer数据结构:

 // Timer代表一次定时,时间到来后仅发生一个事件。
type Timer struct {
C <-chan Time
r runtimeTimer
}

Timer对外仅暴露一个channel,指定的时间到来时就往该channel中写入系统时间,也即一个事件。

使用场景

1. 设定超时时间

有时我们希望从一个管道中读取数据,在管道中没有数据时,我们不想让程序永远阻塞在管道中,而是设定一个超时时间,在此时间段中如果管道中还是没有数据到来,则判定为超时。

比如从一个连接中等待数据,其简单的用法如下代码所示:

func waitChannel(conn <-chan string) bool {
timer := time.NewTimer(1 * time.Second) //设置超时时间 1s select {
case <- conn:
timer.stop() // 接收到数据后,要停止计时器
return true
case timer.C: //超时判断
fmt.Println("WaitChnel timeout!")
return false
}
}

WaitChannel作用就是检测指定的管道中是否有数据到来,通过select语句轮询conn和timer.C两个管道,timer会在1s后向timer.C写入数据,如果1s内conn还没有数据,则会判断为超时。

2. 延迟执行某个方法

有时我们希望某个方法在今后的某个时刻执行,如下代码所示:

func DelayFunction() {
timer := time.NewTimer(5 * time.Second) select {
case <- timer.C:
log.Println("Delayed 5s, start to do something.")
// do something
}
}

DelayFunction()会一直等待timer的事件到来才会执行后面的方法(打印)。

Timer对外接口

1. 创建定时器

使用方法,func NewTimer(d Duration) *Timer 指定一个时间即可创建一个Timer,Timer一经创建便开始集是, 不需要额外的启动命令。

实际上,创建Timer意味着把一个计时任务交给系统守护协程,该协程管理着所有的Timer,当Timer的时间到达后向Timer的管道中发送当前的时间作为事件。详细的实现原理我们后面会单独介绍。

2. 停止定时器

Timer创建后可以随时停止,停止计时器的方法是:

func (t *Timer) Stop() bool

其返回值代表定时器有没有超时:

  • true: true: 定时器超时前停止,后续不会再有事件发送;
  • false: 定时器超时后停止;

实际上,停止计时器意味着通知系统守护协程移除该定时器。详细的实现原理我们后面单独介绍。

3. 重置定时器

已过期的定时器或者已停止的定时器,可以通过重置动作重新激活,重置方法如下:

func (t *Timer) Reset(d Duration) bool

重置的动作实质上是先停掉定时器,再启动。其返回值也是掉计时器的返回值。

需要注意的是,重置定时器虽然可以用于修改还未超时的定时器,但正确的使用方式还是针对已过期的定时器或已被停止的定时器,同时其返回值也不可靠,返回值存在的价值仅仅是与前面版本兼容。

实际上,重置定时器意味着通知系统守护协程移除该定时器,重新设定时间后,再把定时器交给守护协程

其他接口

前面介绍了Timer的标准接口,time包同时还提供了一些简单的方法,在特定的场景下可以简化代码。

1. After()

有时我们就是想等指定的时间,没有需求提前停止定时器,也没有需求复用该定时器,那么可以使用匿名的定时器。

func After(d Duration) <-chan Time方法创建一个定时器,并返回定时器的管道,如下代码所示:

func AfterDemo() {
log.Println(time.Now())
<- time.After(1 * time.Second)
log.Println(time.Now())
}

AfterDemo()两条打印时间间隔为1s,实际还是一个定时器,但代码变得更简洁。

2. AferFunc()

前面我们例子中讲到延迟一个方法的调用,实际上通过AfterFunc可以更简洁。AfterFunc的原型为:

func AfterFunc(d Duration, f func()) *Timer

该方法在指定时间到来后会执行函数f。例如:

func AfterFuncDemo() {
log.Println("AfterFuncDemo start: ", time.Now())
time.AfterFunc(1 * time.Second, func() {
log.Println("AfterFuncDemo end: ", time.Now())
}) time.Sleep(2 * time.Second) // 等待协程退出
}

AfterFuncDemo()中先打印一个时间,然后使用AfterFunc启动一个定器,并指定定时器结束时执行一个方法打印结束时间。

与上面的例子所不同的是,time.AfterFunc()是异步执行的,所以需要在函数最后sleep等待指定的协程退出,否则可能函数结束时协程还未执行。

总结

Timer内容总结如下:

  • time.NewTimer(d)创建一个Timer;
  • timer.Stop()停掉当前Timer;
  • timer.Reset(d)重置当前Timer;

Timer实现原理

前言

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

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

数据结构

1. Timer

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

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

Timer只有两个成员:

  • C: 管道,上层应用根据此管道接收事件;
  • r: runtime定时器,该定时器即系统管理的定时器,对上层应用不可见;

这里应该按照层次来理解Timer数据结构,Timer.C即面向Timer用户的,Timer.r是面向底层的定时器实现。

2.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,即完成了一次定时任务。

1. 创建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示意图如下:

2. 停止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示意图如下:

3. 重置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;

参考

地鼠编程

Go定时器--Timer的更多相关文章

  1. python中实现定时器Timer

    实现定时器最简单的办法是就是循环中间嵌time.sleep(seconds), 这里我就不赘述了 # encoding: UTF-8 import threading #Timer(定时器)是Thre ...

  2. 订餐系统之定时器Timer不定时

    经过几天漫长的问题分析.处理.测试.验证,定时器Timer终于定时了,于是开始了这篇文章,希望对还在纠结于“定时器Timer不定时”的同学有所帮助,现在的方案,在系统日志中会有警告,如果您有更好的方案 ...

  3. PHP框架Swoole的一个定时器Timer特性

    在各种业务型系统中,往往需要服务器在后台扫描相关数据,触发相应的统计.通知等操作. 比如对于一个项目管理系统,需要每天的特定时间内,统计每项任务的执行.到期情况.整个项目的进度等等,根据统计情况,做相 ...

  4. C#创建windows服务搭配定时器Timer使用实例(用代码做,截图版)

       功能说明:C#创建一个windows服务,服务启动时D:\mcWindowsService.txt写入数据,服务运行期间每隔两秒写入当前时间. 原理这些就不说了,三语两语说不清楚,直接贴一个实例 ...

  5. iOS 定时器Timer常见问题

    最近有朋友问我使用NStimer遇见与ScrollView并存时存在主线程阻塞的问题,自己总结几种解决方法: 问题原因: 一般定时器timer都会被以默认模式default添加到主线程的runloop ...

  6. 定时器Timer不定时

    订餐系统之定时器Timer不定时 经过几天漫长的问题分析.处理.测试.验证,定时器Timer终于定时了,于是开始了这篇文章,希望对还在纠结于“定时器Timer不定时”的同学有所帮助,现在的方案,在系统 ...

  7. 多线程&定时器Timer&同步&线程通信&ThreadLocal

    1.多线程 线程状态分为:新建状态.就绪状态.运行状态.阻塞状态.死亡状态 对象等待池的阻塞状态:运行状态执行了wait方法 对向锁池的阻塞状态:试图获得某个同步锁,已经被其他线程占用,就会放到对象的 ...

  8. Java进阶(十八)Java实现定时器(Timer)

    Java实现定时器(Timer) 绪 在应用开发中,经常需要一些周期性的操作,比如每5分钟执行某一操作等.对于这样的操作最方便.高效的实现方式就是使用java.util.Timer工具类.java.u ...

  9. 定时器Timer&ScheduledThreadPoolExecutor

    定时器Timer&ScheduledThreadPoolExecutor /** * @ClassName: TimerTest * @author: daniel.zhao * @date: ...

  10. JAVA多线程提高一:传统线程技术&传统定时器Timer

    前面我们已经对多线程的基础知识有了一定的了解,那么接下来我们将要对多线程进一步深入的学习:但在学习之前我们还是要对传统的技术进行一次回顾,本章我们回顾的则是:传统线程技术和传统的定时器实现. 一.传统 ...

随机推荐

  1. Java 将Word转为Tiff

    本文以Java代码示例展示如何将Word文档转换为Tiff格式.代码方法比较简单,加载Word文档,并通过saveToTiff(tiffFilename)方法直接保存为tiff格式即可. 1.Java ...

  2. [考试总结]noip模拟16

    达成成就,一天更3篇总结. 又是一个暴力场 别问我为什么开局 \(5\) 分钟就问老师为什么 \(T3\) 没有提交的窗口. 开题读题,一路自闭到 \(T3\) ,发现 \(T3\) 可打暴力,所以一 ...

  3. Tomcat 性能监控与优化

    JMX JMX(Java Management Extensions)是一个为应用程序植入管理功能的框架.JMX是一套标准的 代理和服务,实际上,用户可以在任何Java应用程序中使用这些代理和服务实现 ...

  4. 【Maven实战技巧】「插件使用专题」Maven-Assembly插件实现自定义打包

    前提概要 最近我们项目越来越多了,然后我就在想如何才能把基础服务的打包方式统一起来,并且可以实现按照我们的要求来生成,通过研究,我们通过使用maven的assembly插件完美的实现了该需求,爽爆了有 ...

  5. js 倒计时退出

    <p>您已提交成功!<span id='submit_second'></span></p> $("#submit_second") ...

  6. 从事IT行业 vs 玩卡牌手游

    我曾经玩过一款手游,它一直不停地推出新的更高品质的道具和人物,新出的东西比旧的东西拥有更强力的功能,要想保持领先就必须一直充钱出新东西才行.不管你以前充了十几万还是几十万,如果有半年不充,那就会被新区 ...

  7. [CTF] CTF入门指南

    CTF入门指南 何为CTF ? CTF(Capture The Flag)夺旗比赛,在网络安全领域中指的是网络安全技术人员之间进行技术竞技的一种比赛形式.CTF起源于1996年DEFCON全球黑客大会 ...

  8. RHCE_DAY01

    shell概述 shell是一个程序,它连接了用户和Linux内核,它可以解释用户输入的命令传递给内核,让用户可以更加方便的使用Linux系统 shell 本身并不是内核的一部分,它只是站在内核的基础 ...

  9. 可怕!字节跳动大牛爆出的“Android进阶指南”,强到无法形容,GitHub已标星“8.5k”!

    前言 从去年8月份开始,我开始重新找工作,在这期间刷了不少面试题和大牛博客,无意中薅到一份字节跳动大牛整理的一份"Android进阶指南",包含Android面试常见问题.主流技术 ...

  10. 【Lua篇】静态代码扫描分析(一)初步介绍

    一.静态代码分析         静态代码分析是一种通过检查代码而不是执行程序来发现源代码中错误的手段.通常可以帮助我们发现常见的编码错误,例如: 语法错误 违反制定的标准编码 未定义的变量 安全性问 ...