Go组件学习——cron定时器
1 前言
转到Go已经将近三个月,写业务代码又找到了属于Go的条件反射了。
后置声明和多参数返回这些Go风格代码写起来也不会那么蹩脚,甚至还有点小适应~
反而,前几天在写Java的时候,发现Java怎么启动这么慢,Java怎么能够容忍这些用不到的代码还理直气壮的躺在那……等等,这些话在哪听过类似的???
“Go为什么要后置声明,多别扭啊”
“Go里面为啥要定义这么多的struct,看的头晕”
……
其实,没有最好的语言,只有最适合的。
前面《Go语言学习》系列主要介绍了一些Go的基础知识和相较于Java的一些新特性。后续如果有相关的体会和新的还会继续更新。
从这篇开始,开始学习Go的一些工具类库和开源组件,希望在学习这些优秀的开源项目过程中,更深入的了解Go,发现Go的威力。
2 cron简介
robfig/cron是一个第三方开源的任务调度库,也就是我们平时说的定时任务。
Github:https://github.com/robfig/cron
官方文档:https://godoc.org/github.com/robfig/cron
3 cron如何使用
1、新建文件cron-demo.go
package main import (
"fmt"
"github.com/robfig/cron"
"time"
) func main() {
c := cron.New()
c.AddFunc("*/3 * * * * *", func() {
fmt.Println("every 3 seconds executing")
}) go c.Start()
defer c.Stop() select {
case <-time.After(time.Second * 10):
return
}
}
cron.New创建一个定时器管理器
c.AddFunc添加一个定时任务,第一个参数是cron时间表达式,第二个参数是要触发执行的函数
go c.Start()新启一个协程,运行定时任务
c.Stop是等待停止信号结束任务
2、在cron-demo.go文件下执行go build
本项目采用go mod进行包管理,所以执行go build命令后,会在go.mod文件中生成对应的依赖版本如图所示

3、运行cron-demo.go

可以看出每3秒执行一次,直到10秒后过期退出进程,任务结束。
代码参见项目:go-demo项目(https://github.com/DMinerJackie/go-demo/tree/master/main/src/cron)
看上去这个任务调度还是蛮好用的,那么具体是如何实现的呢,看了下源码,也是非常的短小精悍,目录结构如下。

下面通过几个问题一起看下cron是如何实现任务调度。
4 cron如何解析任务表达式
上例我们看到添加“*/3 * * * * *”这样的表达式,就能实现每3秒执行一次。
显然,这个表达式只是对人友好的一种约定表达形式,要真正在指定时间执行任务,cron肯定是要读取并解析这个c表达式,转化为具体的时间再执行。
那我们来看看,这个具体是如何执行的。
进入AddFunc函数实现
// AddFunc adds a func to the Cron to be run on the given schedule.
func (c *Cron) AddFunc(spec string, cmd func()) error {
return c.AddJob(spec, FuncJob(cmd))
}
这只是套了个壳,具体还要进入AddJob函数
// AddJob adds a Job to the Cron to be run on the given schedule.
func (c *Cron) AddJob(spec string, cmd Job) error {
schedule, err := Parse(spec)
if err != nil {
return err
}
c.Schedule(schedule, cmd)
return nil
}
该函数第一行就是解析cron表达式,顺藤摸瓜,我们看到具体实现如下
// Parse returns a new crontab schedule representing the given spec.
// It returns a descriptive error if the spec is not valid.
// It accepts crontab specs and features configured by NewParser.
func (p Parser) Parse(spec string) (Schedule, error) {
if len(spec) == 0 {
return nil, fmt.Errorf("Empty spec string")
}
if spec[0] == '@' && p.options&Descriptor > 0 {
return parseDescriptor(spec)
} // Figure out how many fields we need
max := 0
for _, place := range places {
if p.options&place > 0 {
max++
}
}
min := max - p.optionals // Split fields on whitespace
fields := strings.Fields(spec) // 使用空白符拆分cron表达式 // Validate number of fields
if count := len(fields); count < min || count > max {
if min == max {
return nil, fmt.Errorf("Expected exactly %d fields, found %d: %s", min, count, spec)
}
return nil, fmt.Errorf("Expected %d to %d fields, found %d: %s", min, max, count, spec)
} // Fill in missing fields
fields = expandFields(fields, p.options) var err error
field := func(field string, r bounds) uint64 { // 抽象出filed函数,方便下面调用
if err != nil {
return 0
}
var bits uint64
bits, err = getField(field, r)
return bits
} var (
second = field(fields[0], seconds)
minute = field(fields[1], minutes)
hour = field(fields[2], hours)
dayofmonth = field(fields[3], dom)
month = field(fields[4], months)
dayofweek = field(fields[5], dow)
)
if err != nil {
return nil, err
} return &SpecSchedule{
Second: second,
Minute: minute,
Hour: hour,
Dom: dayofmonth,
Month: month,
Dow: dayofweek,
}, nil
}
该函数主要是将cron表达式映射为“Second, Minute, Hour, Dom, Month, Dow”6个时间维度的结构体SpecSchedule。
SpecSchedule是实现了方法“Next(time.Time) time.Time”的结构体,而“Next(time.Time) time.Time”是定义在Schedule接口中的
// The Schedule describes a job's duty cycle.
type Schedule interface {
// Return the next activation time, later than the given time.
// Next is invoked initially, and then each time the job is run.
Next(time.Time) time.Time
}
所以,最终可以理解是将cron解析后转换为下一次要执行的时刻,等待执行。
5 cron如何执行任务
我们知道通过parser.go可以将人很好理解的表达式转换为cron可以读懂的要执行的时间。
有了要执行的时间点,那么cron具体是如何执行这些任务的呢?
我们看下Start函数的具体实现
// Start the cron scheduler in its own go-routine, or no-op if already started.
func (c *Cron) Start() {
if c.running {
return
}
c.running = true
go c.run()
}
这里会通过判定Cron的running字段是否在运行来巨额听是否要启动任务。
显然这里running是false,因为在调用c.New初始化的时候running被设置为false。
所以,这里新启一个协程用于执行定时任务,再次顺藤摸瓜,我们看到run函数的实现
// Run the scheduler. this is private just due to the need to synchronize
// access to the 'running' state variable.
func (c *Cron) run() {
// Figure out the next activation times for each entry.
now := c.now()
for _, entry := range c.entries {
entry.Next = entry.Schedule.Next(now)
} for {
// Determine the next entry to run.
sort.Sort(byTime(c.entries)) var timer *time.Timer
if len(c.entries) == 0 || c.entries[0].Next.IsZero() { // 如果没有要执行的任务或者第一个任务的待执行时间为空,则睡眠
// If there are no entries yet, just sleep - it still handles new entries
// and stop requests.
timer = time.NewTimer(100000 * time.Hour)
} else {
timer = time.NewTimer(c.entries[0].Next.Sub(now)) // 否则新建一个距离现在到下一个要触发执行的Timer
} for {
select {
case now = <-timer.C: // 触发时间到,执行任务
now = now.In(c.location)
// Run every entry whose next time was less than now
for _, e := range c.entries {
if e.Next.After(now) || e.Next.IsZero() {
break
}
go c.runWithRecovery(e.Job)
e.Prev = e.Next
e.Next = e.Schedule.Next(now)
} case newEntry := <-c.add: // 添加任务
timer.Stop()
now = c.now()
newEntry.Next = newEntry.Schedule.Next(now)
c.entries = append(c.entries, newEntry) case <-c.snapshot: // 调用c.Entries()返回一个现有任务列表的snapshot
c.snapshot <- c.entrySnapshot()
continue case <-c.stop: // 任务结束,退出
timer.Stop()
return
} break
}
}
}
进入该函数,首先遍历所以任务,找到所有任务下一个要执行的时间。
然后进入外层for循环,对于各个任务按照执行时间进行排序,保证离当前时间最近的先执行。
再对任务列表进行判定,是否有任务如果没有,则休眠,否则初始化一个timer。
里层的for循环才是重头戏,下面主要分析这个for循环里面的任务加入和执行。
在此之前,需要了解下go标准库的timer
timer用于指定在某个时间间隔后,调用函数或者表达式。
使用NewTimer就可以创建一个Timer,在指定时间间隔到达后,可以通过<-timer.C接收值。
package main import (
"fmt"
"time"
) func main() {
timer1 := time.NewTimer(2 * time.Second) <-timer1.C
fmt.Println("Timer 1 expired") timer2 := time.NewTimer(time.Second)
go func() {
<-timer2.C
fmt.Println("Timer 2 expired")
}() stop2 := timer2.Stop()
if stop2 {
fmt.Println("Timer 2 stopped")
}
}
执行结果为
Timer 1 expired
Timer 2 stopped
timer1表示2秒后到期,在此之前都是阻塞状态,2秒后<-timer1.C接收到信号,执行下面的打印语句。
timer2表示1秒后到期,但是中途被Stop掉了,相当于清除了定时功能。
有了这个背景之后,我们再来看run函数的里层for循环。
接收到c.add信道
case newEntry := <-c.add: // 添加任务
timer.Stop()
now = c.now()
newEntry.Next = newEntry.Schedule.Next(now)
c.entries = append(c.entries, newEntry)
将timer停掉,清除设置的定时功能,并以当前时间点为起点,设置添加任务的下一次执行时间,并添加到entries任务队列中。
接收到timer.C信道
case now = <-timer.C: // 触发时间到,执行任务
now = now.In(c.location)
// Run every entry whose next time was less than now
for _, e := range c.entries {
if e.Next.After(now) || e.Next.IsZero() {
break
}
go c.runWithRecovery(e.Job)
e.Prev = e.Next
e.Next = e.Schedule.Next(now)
}
当定任务到点后,time.C就会接收到值,并新开协程执行真正需要执行的Job,之后再更新下一个要执行的任务列表。
我们进入runWithRecovery函数,该函数从函数名就可以看出,即使出现panic也可以重新recovery,保证其他任务不受影响。
func (c *Cron) runWithRecovery(j Job) {
defer func() {
if r := recover(); r != nil {
const size = 64 << 10
buf := make([]byte, size)
buf = buf[:runtime.Stack(buf, false)]
c.logf("cron: panic running job: %v\n%s", r, buf)
}
}()
j.Run()
}
追根溯源,我们发现真正执行Job的是j.Run()的执行。进入这个Run函数的实现,我们看到
func (f FuncJob) Run() { f() }
没错,我们要执行的任务一直从AddFunc一直往下传递,直到这里,我们通过调用Run函数,将包装的FuncJob类型的函数通过f()的形式进行执行。
这里说的可能比较模糊,举个例子,Go里面的闭包定义
func () {
fmt.Println("test")
}()
如果这里定义后面没有"()"该函数就不会执行,所以结合这个看上面的定时任务是如何执行就更容易理解了。
6 代码阅读体会
1、channel的奥妙
通过channel可以让感知变得轻而易举,比如timer.C就像是时间到了,自然会有人来敲门告诉你。而不需要我们自己主动去获取是否到期了。
2、常用类库的使用
比如在parser里面我们看到了"fields := strings.Fields(spec)",在日常开发中,我们可以灵活使用这些API,避免自己造轮子的情况。
3、多思考
之前做Java的时候,更多的是沉浸在各种工具和框架的使用,对于这些工具和框架的实现关注的不多。比如从Quartz到Spring Job,我们需要更新的是越来越好用的定时任务工具,而底层的实现升级Spring都帮我们考虑好了。这种对业务对项目有友好的,可以快速的实现业务功能开发,但是对于开发者并不友好,友好的设计麻痹了开发者对于底层原理的深究的欲望。
Go组件学习——cron定时器的更多相关文章
- C# BackgroundWorker组件学习入门介绍
C# BackgroundWorker组件学习入门介绍 一个程序中需要进行大量的运算,并且需要在运算过程中支持用户一定的交互,为了获得更好的用户体验,使用BackgroundWorker来完成这一功能 ...
- JavaScript学习05 定时器
JavaScript学习05 定时器 定时器1 用以指定在一段特定的时间后执行某段程序. setTimeout(): 格式:[定时器对象名=] setTimeout(“<表达式>”,毫秒) ...
- bootstrap组件学习
转自http://v3.bootcss.com/components/ bootstrap组件学习 矢量图标的用法<span class="glyphicon glyphicon-se ...
- C# BackgroundWorker组件学习
C# BackgroundWorker组件学习 C# BackgroundWorker组件学习 一个程序中需要进行大量的运算,并且需要在运算过程中支持用户一定的交互,为了获得更好的用户体验,使用Ba ...
- [Python] wxPython 状态栏组件、消息对话框组件 学习总结(原创)
1.状态栏组件 1.基本介绍 上图: 红框框内的就是状态栏. 他可以分成若干个区块,比如上者分为了两个区块,并且比例是固定的,创建时可以指定 每个区块都能够显示 信息,一般通过 绑定事件 实时更新 各 ...
- Vue组件学习
根据Vue官方文档学习的笔记 在学习vue时,组件学习比较吃力,尤其是组件间的通信,所以总结一下,官方文档的组件部分. 注册组件 全局组件 语法如下,组件模板需要使用一个根标签包裹起来.data必须是 ...
- JMeter学习-021-JMeter 定时器的应用
定时器类型 下面我们看下jmeter提供了哪些定时器组件: 固定定时器 高斯随机定时器 Uniform Random Timer Synchronizing Timer Poisson Random ...
- vue组件学习(二)
父子组件之间的数据传递, 父传给子: 直接在组件上传,如:<count :number="2"> (冒号和不要冒号的区别,有冒号会自动转为相应的类型)在名为count的 ...
- vue组件学习(一)
1, vue中的 is 的用法,有时候我们需要把一个组件绑定到指定的标签下,比如把tr组件放到table下,直接这样写是不行的, <!DOCTYPE html> <html lang ...
随机推荐
- Tuxera NTFS 2018 for Mac中文破解版 U盘读写软件-让你的Mac支持NTFS
下载链接(复制到浏览器下载):http://h5ip.cn/TLMc 软件介绍 给大家带来一款苹果Mac上如何使用U盘读写的软件,Tuxera NTFS 2018 for Mac中文破解版,Mac O ...
- export命令的使用
一:export将环境变量昭告天下 1.直接输入export会将显示bash下的所有环境变量 2.env/set/export/declare都可以显示shell的变量 ...
- Java分割中英文,并且中文不能分割一半?
最近准备入其他坑位.在面试过程中,遇到下面这题笔试题,拿出来分享分享. 题目:编写一个截取字符串的函数,输入为一个字符串和字节数,输出为按字节截取的字符串.但是要保证汉字不被截半个,如“我ABC”4, ...
- GStreamer基础教程03 - 媒体类型与Pad
摘要 在上一篇文章中,我们介绍了如何将多个element连接起来构造一个pipline,进行数据传输.那么GStreamer是通过何种方式保证element之间能正常的进行数据传输?今天就将介绍GSt ...
- 【需要重新维护】Redis笔记20170811视频
很多内容都是抄的,个人记录 1.windows下初见 安装 进入目录 修改配置文件(暂时使用默认,未配置环境变量) 目录下:redis-server.exe启动服务 新建命令提示符,目录下,redis ...
- 苹果二代TWS无线耳机AirPods调研
产品介绍 苹果AirPods二代自从2018年9月份上市以来,到现在将近一年的时间了,据江湖传闻,苹果AirPods的总售卖个数,已经超过了5000W部,这样价格的TWS耳机,能够卖那么多的量,估计也 ...
- [apue] 多进程管道读写的一些疑问
对于一对一的pipe: 1) 写进程关闭写管道后,读进程继续读管道会导致read返回0: 2) 读进程关闭读管道后,写进程继续写管道会激发SIGPIPE信号,若捕获,则write返回-1: 而对于多对 ...
- SSM框架学习笔记_第1章_SpringIOC概述
第1章 SpringIOC概述 Spring是一个轻量级的控制反转(IOC)和面向切面(AOP)的容器框架. 1.1 控制反转IOC IOC(inversion of controller)是一种概念 ...
- 并发编程-concurrent指南-阻塞队列-优先级的阻塞队列PriorityBlockingQueue
PriorityBlockingQueue是一个支持优先级的无界阻塞队列. 它使用了和类 java.util.PriorityQueue 一样的排序规则.你无法向这个队列中插入 null 值. 所有插 ...
- HDU 1828:Picture(扫描线+线段树 矩形周长并)
题目链接 题意 给出n个矩形,求周长并. 思路 学了区间并,比较容易想到周长并. 我是对x方向和y方向分别做两次扫描线.应该记录一个pre变量,记录上一次扫描的时候的长度,对于每次遇到扫描线统计答案的 ...