首页 > 编程语言 >【Go专家编程——定时器】

【Go专家编程——定时器】

时间:2024-06-04 13:29:38浏览次数:27  
标签:定时器 协程 编程 Timer func time Go Ticker

定时器

定时器在Go语言开发中被广泛使用,准确掌握其用法和实现原理至关重要
Go语言提供了两种定时器

  • 一次性定时器Timer:定时器只计时一次,计时结束便停止运行
  • 周期性定时器Ticker:定时器周期性地进行计时,除非主动停止,否则将永久运行。

1 一次性定时器Timer

1.1 简介

Timer是一种单一事件的定时器,即经过指定的时间后触发一个事件,这个事件通过其本身提供的channel进行通知。
源码如下:

type Timer struct{	//Timer代表一次定时,时间到来后仅发生一个事件
	c <-chan Time
	r runtimeTimer
}
//通过timer.NewTimer(d Duration)创建一个Timer,参数是等待的时间,时间结束后触发时间

1.2 使用场景

1)设定超时时间
协程从管道读数据时,如果管道没有数据,将会被一直阻塞。我们不希望协程被一直阻塞,而是等待一个指定的时间,超时后就结束阻塞。

func WaitChannel(conn <-chan string) bool {
	timer := time.NewTimer(1 * time.Second)
	select {
	case <- conn:
		timer.Stop()
		return true
	case <- timer.C:
		println("WaitChannel timeout!")
		return false
	}
}

2)延迟执行某个方法

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

1.3 Timer对外接口

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

创建Timer意味把一个及时任务交给系统守护协程。该协程管理着所有的Timer,当Timer的时间到达后,Timer向管道中发送当前的时间作为事件。
2)停止定时器
Timer创建后可以随时停止:
func (t *Timer) Stop() bool
其返回值代表定时器有没有超时

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

实际上,停止计时器意味着通知系统守护协程移除该定时器

3)重置定时器
已过期的定时器或已停止的计时器可以通过重置动作重新激活,重置方法如下:
func(t *Timer) Reset(d Duration) bool
重置的动作实质上是通知系统守护协程移除该定时器,重新设定时间后,再把定时器交给守护协程。

1.4 简单接口

timer包同时还提供了一些简单的方法,在特定的场景下可以优化代码
1)After()
只是想等待指定的时间,没有提前停止定时器的需求,也没有复用该定时器的需求,那么可以使用匿名的定时器。

func After(d Duration) <-chan Time方法创建一个定时器,返回定时器的管道

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

两条打印的时间间隔为1s。
2)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)	//	等待协程退出
}

上面两个例子都是打印出两个时间,但是time.AfterFunc()是异步执行的,而上一个例子是同步执行的。

2.实现原理

2.1 数据结构

1)Timer

type Timer struct{
	C <-chan Time	//管道,上层应用根据此管道接收事件
	r runtimeTimer		//running定时器,该定时器即系统管理的警示器,对上层应用不可见
}

2)runtimeTimer
创建一个Timer实质上是把一个定时任务交给专门的协程进行监控,这个任务的载体便是runtimeTimer。

简单地讲,没创建一个Timer意味着创建了一个runtimeTimer变量,然后把它交给系统进行监控。我们通过设置runtimeTimer过期后的行为来达到定时的目的。

type runtimeTimer struct{
	tb uintptr			//存储当前定时器的数组地址
	i int				//存储当前定时器的数组下标
	when int64			//当前定时器触发时间
	period int64		//当前定时器周期性触发间隔(对于Timer来说,恒为0)
	f func(interface{}, uintptr)	//定时器触发时执行的回调函数
	arg interface{}		//定时器触发时执行回调函数传递的参数1
	seq	uintptr			//定时器触发时执行回调函数传递的参数2(该参数只在网络收发场景下使用)
}

2.2 实现原理

一个进程中的多个Timer都由底层的协程来管理,为了描述方面,我们把这个协程称为系统协程。

runtimeTimer存放在数组中,并按照when字段对所有的runtimeTimer进行堆排序,定时器触发时执行runtimeTimer中的预定义函数f,即完成了一次定时任务。

1)创建Timer

func NewTimer(d Duration) *Timer{
	c := make(chan Time,1)	//创建一个通道
	t := &Timer{
		C:c,
		r:runtimeTimer{
			when: when(d),	//触发时间
			f: sendTime,	//触发后执行snedTime函数
			arg: c,			//触发后执行sendTime函数时附带的参数
		},
	}
	startTimer(&t,r)	//启动定时器,只是把runtimeTimer放到系统协程的堆中,由系统协程维护
	
	return t
}
  • NewTimer构造了一个Timer
  • 把Timer.r通过startTimer交给系统协程维护
    • when计算下一次定时器触发的绝对时间,即当前时间+d
    • sendTime方法是定时器触发时的动作
func sendTime(c interface{}, seq uintptr){
	select{
	case c.(chan Time) <- Now():
	default:
	}
}

创建Timer时生成的管道含有一个缓冲区(make(chan Time, 1)),所以Timer触发时间管道写入事件永远不会阻塞,sendTime写完即退出。

之所以sendTime()使用select并搭配一个空的default分支,是因为后面的Ticker也复用sendTime(),Ticker触发时也会向管道中写入时间,但无法保证之前的数据已被取走,如果管道中还有值,则本次不再向管道中写入时间,将本次触发的事件直接丢弃。

startTimer(&t.r)的具体实现在runtime包中,其主要作用是把runtimeTimer写入系统协程的数组中,并启动系统协程(之后会介绍)

2)停止Timer

  • stopTimer即通知系统协程吧该Timer移除,即不再监控。
  • 系统协程只是移除Timer,并不会关闭通道,以避免用户协程读取错误
func (t *Timer) Stop() bool{
	return stopTimer(&t.r)
}

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。如果已经触发,返回false

注意:按照官方的说法,Reset()应该作用于已经停止的Timer或已经触发Timer。其返回值应该总是false,如果不按照次约定使用Reset(),则有可能遇到Reset()和Timer触发后同时执行的情况,此时有可能会收到两个事件,对应用程序造成负面影响。

2.3 小结

  • NewTimer()创建一个新的Timer交给系统协程监控
  • Stop()通知系统协程删除指定的Timer
  • Reset()通知系统协程删除指定的Timer并再添加一个新的Timer

3. 周期性定时器(Ticker)

3.1 简介

Ticker是一种周期性定时器,即周期性触发一个事件,这个事件通过其本身提供的channel进行通知,对外仅暴露一个channel。
源码如下:

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

3.2 使用场景

1)简单的定时任务
下面的代码演示每隔1s记录一次日志

func TickerDemo() {
	timer := time.NewTicker(1 * time.Second)
	defer ticker.Stop()

	for range ticker.C{
		log.Println("Ticker tick.")
	}
}

2)定时聚合任务
有时我们希望把一些任务打包进行批量处理。比如,公交车发车场景:

  • 公交车每隔5分钟发一班,不管是否已坐满乘客
  • 已坐满乘客情况下,不足5分钟也发车
func TickerLanch() bool {
	ticker := time.NewTicker(5 * time.Second)
	defer ticker.Stop()
	maxPassenger := 30	//没车最大装载人数
	passengers := make([]string,0,maxPassenger)
	
	for {
		passenger := GetNewPassenger()//获取一个新乘客
		if passenger != ""{
			passenger = append(passengers,passenger)
		}else {
			time.Sleep(1*time.Second)
		}

		select {
		case <- ticker.C:		// 时间到,发车
			Launch(passengers)
			passengers = []string{}
		default:
			if len(passengers) >= maxPassenger{//时间没到,车已满座,发车
				Launch(passengers)
				passengers = []string{}
			}
		}
	}

}

3.3 Timer对外接口

1)创建定时器
func NewTicker(d Duration) *Ticker方法指定一个时间即可创建一个Ticker
参数d为定时器事件触发的周期

2)停止定时器
Ticker创建后可以随时停止:
func (t *Ticker) Stop()

该方法会停止计时,意味着不会向定时器的管道中写入事件,但管道并不会被关闭。管道在使用完,生命周期结束后自动释放。

Ticker在使用完后务必要释放,否则会产生资源泄漏,会持续消耗CPU资源

3.4 简单接口

timer包同时还提供了一些简单的方法,在特定的场景下可以优化代码
1)Tick()
func Tick(d Duration) <-chan Time返回定时器的管道
函数的内部创建了一个Ticker,但并不会返回,所以没有手段来停止该Ticker

3.5 小结

Ticker的相关内容总结如下:

  • 使用time.NewTicker()创建一个定时器
  • 使用Stop停止一个定时器
  • 定时器使用完毕要释放,否则会产生资源泄漏

4.实现原理

Ticker与之前讲的Timer几乎完全相同,数据结构和实现内部机制都相同,唯一不同的是创建方式。

4.1 数据结构

1)Ticker
数据结构和Timer完全一致

type Ticker struct{
	c <-chan Time
	r runtimeTimer
}

2)runtimeTimer
也与Timer完全一致

4.2 实现原理

1)创建Ticker

func NewTicker(d Duration) *Ticker{
	if d <= 0{
		panic(errors.New("non-positive interval for NewTicker"))
	}
	c := make(chan Time, 1)
	t := &Ticker{
		c: c,
		r: runtimetimer{
			when: when(d),
			//根据这个产生决定Timer是一次性的,还是周期性的
			period: int64(d),//Ticker跟Timer的重要区别就上提供了period参数
			f: sendTime,
			arg: c,
		}
	}
	startTime(&t.r)
	return t
}

NewTicker()只是构造了一个Ticker,然后吧Ticker.r通过startTime交给了系统协程维护,其中period为事件触发的周期,sendTime方法便是定时器触发时的动作

func sendTime(c interface{}, seq uintptr){
	select{
	case c.(chan Time) <- Now():
	default:
	}
}

sendTime函数中每隔一段时间就向管道中写入当前时间,如果缓冲区内的数据未被即时取走,那么也不会阻塞,而是直接退出,会造成本次事件丢失的后果。

2)停止Ticker
停止Ticker时只是简单地把Ticker从系统协程中移除,但并不会关闭通道,以免用户协程读取错误。

func (t *Ticker) Stop(){
	stopTimer(&t.r)
}

该函数不需要返回值。

4.3 小结

  • NewTicker()创建一个新的Ticker交给系统协程监控
  • Stop()通知系统协程删除指定的Ticker

标签:定时器,协程,编程,Timer,func,time,Go,Ticker
From: https://blog.csdn.net/qq_45467608/article/details/139350076

相关文章

  • Django使用正则表达式
    本书1-7章样章及配套资源下载链接:https://pan.baidu.com/s/1OGmhHxEMf2ZdozkUnDkAkA?pwd=nanc 源码、PPT课件、教学视频等,可以从前言给出的下载信息下载,大家可以评估一下。在Django框架的新版本(v2.0+)中,URLconf模块虽然更改了配置方式,但它依然可以对老版本进行兼容,兼容的......
  • python基于flask的羽毛球场地管理系统django
    该系统分为用户的预约场地前台、管理员的系统管理后台两部分。预约场地前台功能模块:登录、注册、修改密码、选择时间、选择场地、支付费用、生成支付凭证等。系统管理后台功能模块:场地的增删查改,完善用户信息数据,统计场地信息,管理用户等。整个系统各个模块的具体功能有:预约......
  • 用 cgo 处理信号,go 不能处理信号
    下面是我的main.gopackagemain/*#include<stdio.h>;#include<signal.h>;#include<string.h>;#include<stdlib.h>;#include<unistd.h>;//用于存储旧操作的全局变量structsigactionold_action;//信号处理函数voidhandler(intsignum,siginf......
  • Python编程学习第一篇——Python零基础快速入门(五)-列表(List)
    今天我们来一起学习Python的列表(list),Python中的列表(List)是一种有序、可变的数据结构,可以用来存储多个值。列表可以包含不同类型的数据,例如整数、浮点数、字符串等。以下是关于Python列表定义、语法和基本操作的详细示例:1.定义列表:   可以使用方括号[]来定义一个列表。......
  • Netty编程模型入门案例
    在Socket编程模型可以看到发送数据和响应数据直接涉及到的是I/O模型,基于TCP/IP的socket编程使用的是流套接字。那什么是I/O模型呢?简单的理解就是用什么样的通道进行数据的发送和接收——这很大程度上决定了程序通信的性能。下面介绍另一种编程模式——Netty框架的入门案......
  • Django + Vue 使用Nginx + uWsgi部署
    1.settings.py配置STATIC_ROOT=os.path.join(BASE_DIR,'static/dist')#导入前后端静态资源后更改即可DEBUG=True#为True不容易暴露项目信息,当然也不显示BUG信息ALLOWED_HOSTS=['*']STATIC_URL='/static/' 2.django端打包静态资源#会在static下生成[root@dsc1m......
  • 小猴编程周赛C++ | 字符串
    学习C++从娃娃抓起!记录下在学而思小猴编程学习过程中的题目,记录每一个瞬间。侵权即删,谢谢支持!附上汇总贴:小猴编程C++|汇总-CSDN博客【题目描述】小猴最近学习了字符串,为了加强对字符串的理解,猴博士特意给小猴安排了一道编程题:给定一个字符串s,保证s中只包含大写字母(AZ......
  • 小猴编程周赛C++ | 六面世界
    学习C++从娃娃抓起!记录下在学而思小猴编程学习过程中的题目,记录每一个瞬间。侵权即删,谢谢支持!附上汇总贴:小猴编程C++|汇总-CSDN博客【题目描述】六面世界的地图由六边形格子组成,地图一共n行,奇数行有m格,偶数行有m-1格。下图是一个n=5,m=5的地图。小明想从起点S走到终点......
  • 网络编程介绍(二)(端口、通信协议)
    端口标记正在计算机设备上运行的应用程序的,被规定为一个16位的二进制,范围是0~65535。分类周知端口:0~1023,被预先定义的知名应用占用(如:HTTP占用80,FTP占用21)注册端口:1024~49151,分配给用户进程或某些应用程序。动态端口:49152到65535,之所以称......
  • 《信息学奥赛一本通 编程启蒙C++版》3126-3130(5题)
    3126:练21.3 神奇装置信息学奥赛一本通-编程启蒙(C++版)在线评测系统练21.3神奇装置信息学奥赛一本通-编程启蒙(C++版)在线评测系统3126:练21.3神奇装置_哔哩哔哩_bilibili#include<bits/stdc++.h>usingnamespacestd;intmain(){ inta,b,c,d; cin>>a>>b>>c......