前言

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. 支付二维码整合 - 三码合一支持支付宝、QQ、微信

    支付二维码整合 - 三码合一支持支付宝.QQ.微信 1. 前提:获取各个二维码的具体内容 在写代码前,我们需要先获取不同支付方式的二维码内容.很简单,只需要打开各个支付码,截图,然后随便找个可以扫码的 ...

  2. 🏆【Java技术专区】「并发编程专题」教你如何使用异步神器CompletableFuture

    前提概要 在java8以前,我们使用java的多线程编程,一般是通过Runnable中的run方法来完成,这种方式,有个很明显的缺点,就是,没有返回值.这时候,大家可能会去尝试使用Callable中的 ...

  3. 手写RPC

    服务端代码 package com.peiyu.rpcs.bios; import java.io.IOException; public interface IRpcServers { void s ...

  4. npm命令,nrm命令,n命令, nvm命令

    npm命令 npm/yarn config set registry https://registry.npm.taobao.org 设置淘宝镜像npm/yarn config get registr ...

  5. vulnhub-XXE靶机

    仅供个人娱乐 靶机信息 靶机下载地址:https://download.vulnhub.com/xxe/XXE.zip 一.主机探测 二.端口服务识别 nmap -sV 192.168.181.149 ...

  6. Hadoop 3.1.1 - 概述 - 总览

    Apache Hadoop 3.1.1 和之前发布的 3.0.X 版本线相比,Apache Hadoop 3.1.1 吸收了许多重要的改进. 总览 建议用户阅读完整的版本说明.本文提供了对主要变动的总 ...

  7. LAMP和LNMP环境搭建的艰辛历程

    目录 1. LAMP环境的搭建 1. Apache 安装apache遇到的问题 2. mysql 登录mysql的方法 3. PHP 2. Lnmp环境的搭建 1. nginx 2. PHP 3. 配 ...

  8. Session与Cookie的原理以及使用小案例>从零开始学JAVA系列

    目录 Session与Cookie的原理以及使用小案例 Cookie和Session所解决的问题 Session与Cookie的原理 Cookie的原理 Cookie的失效时机 小提示 Session ...

  9. Java 14 新功能介绍

    不做标题党,认认真真写个文章. 文章已经收录在 Github.com/niumoo/JavaNotes 和未读代码博客,点关注,不迷路. Java 14 早在 2019 年 9 月就已经发布,虽然不是 ...

  10. 解决win10快速访问不能取消固定

    最近发现win10的快速访问不能取消固定,比如ftp和smb之类的都不能取消固定 最后百度了一下发现一个简易的方法: 在文件资源管理器地址栏输入:%APPDATA%\Microsoft\Windows ...