golang定时器相关的函数超硬核解析
golang定时器相关的函数超硬核解析
一、前言
Golang 定时器包括:一次性定时器(Timer)和周期性定时器(Ticker)。
编程中经常会通过timer和ticker、AfterFunc。定时器NewTicker是设定每隔多长时间触发的,是连续触发,而计时器NewTimer是等待多长时间触发的,只触发一次,两者是不同的。等待时间函数AfterFunc是在After基础上加了一个回调函数,是等待时间到来后在另外一个goroutine协程里调用。
1.1 定时器相关得函数
-
1、time.NewTicker():创建一个Ticker类型的定时器。
-
2、time.Ticker.C:返回一个定时的通道,每隔一段时间发送一个时间值。
-
3、time.Ticker.Stop():停止定时器。
-
4、time.NewTimer():创建一个Timer类型的定时器。
-
5、time.Timer.C:返回一个通道,定时器到期后发送一个时间值。
-
6、time.Timer.Reset():重新设置定时器到期时间。
-
7、time.Timer.Stop():停止定时器。
二、详细说明
2.1 Timer
2.1.1 说明
timer创建有两种方式,time.NewTimer(Duration) 和time.After(Duration)。后者只是对前者的一个包装。
timer到固定时间后会执行一次,请注意是一次,而不是多次。但是可以通过reset来实现每隔固定时间段执行。使用timer定时器,超时后需要重置,才能继续触发。
2.1.2 Timer案例
import (
"fmt"
"time"
)
func main() {
myTimer := time.NewTimer(time.Second * 10) // 启动定时器
var i int = 0
select {
case <-myTimer.C:
i++
fmt.Println("count: ", i)
}
}
2.1.3 Timer数据结构
type Timer struct {
C <-chan Time // 抛出来的channel,给上层系统使用,实现定时
r runtimeTimer // 给系统管理使用的定时器,系统通过该字段确定定时器是否到时,如果到时,调用对应的函数向C中推送当前时间。
}
-
当timer 过期后,当前时间将会被发送到C
-
timer 只能被NewTimer 或者 AfterFunc两个函数创建
type runtimeTimer struct {
pp uintptr
when int64 //什么时候触发timer
period int64 //如果是周期性任务,执行周期性任务的时间间隔
f func(interface{}, uintptr) // NOTE: must not be closure//到时候执行的回调函数
arg interface{} //执行任务的参数
seq uintptr//回调函数的参数,该参数仅在 netpoll 的应用场景下使用。
nextwhen int64//如果是周期性任务,下次执行任务时间
status uint32//timer 的状态
}
那么定时器是如何实现的呢?首先看一下定时器的构造:
//创建一个将会在Duration 时间后将那一刻的时间发生到C 的timer
func NewTimer(d Duration) *Timer {
c := make(chan Time, 1) //创建1个channel
t := &Timer{ //创建一个timer
C: c,
r: runtimeTimer{
when: when(d), //什么时候执行
f: sendTime, //到时候执行的回调函数
arg: c,//执行参数
},
}
startTimer(&t.r) //开始timer
return t
}
-
C 是一个带1个容量的chan,这样做有什么好处呢,原因是chan 无缓冲发送数据就会阻塞,阻塞系统协程,这显然是不行的。
-
回调函数设置为sendTime,执行参数为channel,sendTime就是到点往C 里面发送当前时间的函数
//c interface{} 就是NewTimer 赋值的参数,就是channel
func sendTime(c interface{}, seq uintptr) {
select {
case c.(chan Time) <- Now(): //写不进去的话,C 已满,走default 分支
default:
}
}
-
sendTime 是不阻塞的,在Timer 实现里面是不会被阻塞的,因为只写一次数据。但是在Ticker里面就会存在阻塞,因为容量为1,ticker 会按时间间隔周期性的写数据到C,这时候如果没有写进去,这次写事件就会丢弃。那么是怎么做到呢?
case c.(chan Time) <- Now() 的时候,如果C 里面的数据没人取走,那么C 已满,case 这条分支发送数据到C就会执行失败而走下面的default。相当于本次调用没有任何操作。 -
官方注释说:如果reader读C数据慢于第二次向C写数据,那么丢掉这次数据是理想的行为。
2.2 Ticker
2.2.1 说明
ticker只要定义完成,从此刻开始计时,不需要任何其他的操作,每隔固定时间都会触发。它会以一个间隔(interval)往通道发送当前时间,而通道的接收者可以以固定的时间间隔从通道中读取时间。
2.2.2 案例说明
package main
import (
"fmt"
"time"
)
func main() {
t:=time.NewTicker(1*time.Second)
defer t.Stop()
for now:=range t.C{
fmt.Println(now)
}
}
周期性定时器到期了之后同样是执行sendTime方法,这个上面已经描述过了。细心的你肯定注意到了,在tickerDemo中有一个defer去停止ticker,为什么要这么做呢?前面分析的时候讲到,创建定时器就是把定时器的runtimeTimer放到由维护协程维护的堆中,一次性定时器到期后,会从堆中删除,如果没有到期则调用Stop方法实现删除。但是,周期性定时器是不会执行删除动作的,所以如果项目里面持续创建多个周期性定时器并没有stop的话,会导致堆越来越大,从而引起资源泄露。
经过代码验证:time.NewTicker定时触发执行任务,当下一次执行到来而当前任务还没有执行结束时,会等待当前任务执行完毕后再执行下一次任务。
2.2.3 数据结构
type Ticker struct {
C <-chan Time //chan 定时到了以后,go 系统会忘里面添加一个当前时间的数据
r runtimeTimer
}
创建一个Ticker
func NewTicker(d Duration) *Ticker {
if d <= 0 {
panic(errors.New("non-positive interval for NewTicker"))
}
//这里预留一个缓冲给timer 一样,但是满了以后没人接收后面会丢掉事件
c := make(chan Time, 1)
t := &Ticker{
C: c,
r: runtimeTimer{
when: when(d),
period: int64(d),
f: sendTime, //和ticker 的函数一样
arg: c,
},
}
startTimer(&t.r)
return t
}
和timer 创建方式一样,只不过period为Duration,这样底层在检查时会根据这个字段判断是不是周期性timer,从而删掉原来的timer,创建新的timer
2.3 总结
2.3.1 特殊说明
上面的两种计时器都会在底层创建一个runtimeTimer,所以每一个版本中runtimeTimer的优化都十分重要
-
Go 1.9版本之前,使用全局唯一的四叉堆维护
-
Go 1.10-1.13,全局使用64个四叉堆,每个处理器(P)对应一个四叉堆
-
Go 1.14版本之后,每个处理器P直接管理一个四叉堆,通过网络轮询器触发
2.3.2 四叉堆说明
|希望能帮助你学习到新的知识点,欢迎提出宝贵的意见!!
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全网最简单!3分钟用满血DeepSeek R1开发一款AI智能客服,零代码轻松接入微信、公众号、小程
· .NET 10 首个预览版发布,跨平台开发与性能全面提升
· 《HelloGitHub》第 107 期
· 全程使用 AI 从 0 到 1 写了个小工具
· 从文本到图像:SSE 如何助力 AI 内容实时呈现?(Typescript篇)
2022-01-04 go写入文件
2022-01-04 go反射
2022-01-04 go读取文件
2022-01-04 Go 读文件的 10 种方法
2022-01-04 如何判断一个 interface{} 的值是否为 nil
2021-01-04 k8s pod的4种网络模式最佳实战(externalIPs )
2021-01-04 kubectl常用命令总结