文章目录

  • 简介
  • 快速使用
  • 时间格式
  • 自定义任务
  • 一点源码
  • 自定义时间策略
  • 总结
  • 参考

简介

gron是一个比较小巧、灵活的定时任务库,可以执行定时的、周期性的任务。gron提供简洁的、并发安全的接口。我们先介绍gron库的使用,然后简单分析一下源码。

快速使用

先安装:

$ go get github.com/roylee0704/gron

后使用:

package mainimport ("fmt""sync""time""github.com/roylee0704/gron"
)func main() {var wg sync.WaitGroupwg.Add(1)c := gron.New()c.AddFunc(gron.Every(5*time.Second), func() {fmt.Println("runs every 5 seconds.")})c.Start()wg.Wait()
}

gron的使用比较简单:

  • 首先调用gron.New()创建一个管理器,这是一个定时任务的管理器;
  • 然后调用管理器的AddFunc()或Add()方法向它添加任务,在启动时添加也是可以的,见下文分析;
  • 最后调用管理器的Start()方法启动它。

gron支持两种添加任务的方式,一种是使用无参数的函数,另一种是实现任务接口。上面例子中使用的是前一种方式,实现接口的方式我们后面会介绍。添加任务时通过gron.Every()指定周期任务的间隔,上面添加了一个 5s 的周期任务,每隔 5s 输出一行文字。

需要注意的是,我们使用sync.WaitGroup保证主 goroutine 不退出。因为c.Start()中只是启动了一个 goroutine,如果主 goroutine 退出了,整个程序就停止了。

运行程序,每隔 5s 输出:

runs every 5 seconds.
runs every 5 seconds.
runs every 5 seconds.

该程序需要按下ctrl + c停止!

时间格式

gron接受time.Duration类型的时间间隔,除了time包中定义的基础Second/Minute/Hour,gron中的xtime子包还提供了Day/Week单位的时间。有一点需要注意,gron支持的时间精度为 1s,小于 1s 的间隔是不支持的。除了单位时间间隔,我们还可以使用4m10s这样的时间:

func main() {var wg sync.WaitGroupwg.Add(1)c := gron.New()c.AddFunc(gron.Every(1*time.Second), func() {fmt.Println("runs every second.")})c.AddFunc(gron.Every(1*time.Minute), func() {fmt.Println("runs every minute.")})c.AddFunc(gron.Every(1*time.Hour), func() {fmt.Println("runs every hour.")})c.AddFunc(gron.Every(1*xtime.Day), func() {fmt.Println("runs every day.")})c.AddFunc(gron.Every(1*xtime.Week), func() {fmt.Println("runs every week.")})t, _ := time.ParseDuration("4m10s")c.AddFunc(gron.Every(t), func() {fmt.Println("runs every 4 minutes 10 seconds.")})c.Start()wg.Wait()
}

通过gron.Every()设置每隔多长时间执行一次任务。对于大于 1 天的时间间隔,我们还可以使用gron.Every().At()指定其在某个时间点执行。例如下面的程序,从第二天的22:00开始,每隔一天触发一次,即每天的22:00触发:

func main() {var wg sync.WaitGroupwg.Add(1)c := gron.New()c.AddFunc(gron.Every(1*xtime.Day).At("22:00"), func() {fmt.Println("runs every second.")})c.Start()wg.Wait()
}

自定义任务

实现自定义任务也很简单,只需要实现gron.Job接口即可:

// src/github.com/roylee0704/gron/cron.go
type Job interface {Run()
}

我们需要调用调度器的Add()方法向管理器添加自定义任务:

type GreetingJob struct {Name string
}func (g GreetingJob) Run() {fmt.Println("Hello ", g.Name)
}func main() {var wg sync.WaitGroupwg.Add(1)g1 := GreetingJob{Name: "dj"}g2 := GreetingJob{Name: "dajun"}c := gron.New()c.Add(gron.Every(5*time.Second), g1)c.Add(gron.Every(10*time.Second), g2)c.Start()wg.Wait()
}

上面我们编写了一个GreetingJob结构,实现gron.Job接口,然后创建两个对象g1/g2,一个 5s 触发一次,一个 10s 触发一次。使用自定义任务的方式可以比较好地处理携带状态的任务,如上面的Name字段。

实际上,AddFunc()方法内部也是通过Add()实现的:

// src/github.com/roylee0704/gron/cron.go
func (c *Cron) AddFunc(s Schedule, j func()) {c.Add(s, JobFunc(j))
}type JobFunc func()func (j JobFunc) Run() {j()
}

在AddFunc()内部,将传入的函数转为JobFunc类型,而gron为JobFunc实现了gron.Job接口。是不是与net/http包中的HandleFunc和Handle很像。如果注意观察的话,在很多 Go 语言的代码中都有此类模式。

一点源码

gron的源码只有两个文件cron.go和schedule.go,cron.go中实现添加任务和调度的方法,schedule.go中是时间策略相关的代码。两个文件算上注释一共才 260 行!我们添加的任务在gron内部都是以Entry结构表示的:

type Entry struct {Schedule ScheduleJob      JobNext time.TimePrev time.Time
}

Next为下次执行时间,Prev为上次执行时间,Job是要执行的任务,Schedule为gron.Schedule接口类型,调用其Next()可计算出下次执行的时间点。

管理器使用gron.Cron结构表示:

type Cron struct {entries []*Entryrunning booladd     chan *Entrystop    chan struct{}
}

任务的调度在另外一个 goroutine 中。如果调度未开始,添加任务可直接append到entries切片中;如果调度已开始(Start()方法已调用),需要向通道add发送待添加的任务。任务调度的核心逻辑在Run()方法中:

func (c *Cron) run() {var effective time.Timenow := time.Now().Local()// to figure next trig time for entries, referenced from nowfor _, e := range c.entries {e.Next = e.Schedule.Next(now)}for {sort.Sort(byTime(c.entries))if len(c.entries) > 0 {effective = c.entries[0].Next} else {effective = now.AddDate(15, 0, 0) // to prevent phantom jobs.}select {case now = <-after(effective.Sub(now)):// entries with same time gets run.for _, entry := range c.entries {if entry.Next != effective {break}entry.Prev = nowentry.Next = entry.Schedule.Next(now)go entry.Job.Run()}case e := <-c.add:e.Next = e.Schedule.Next(time.Now())c.entries = append(c.entries, e)case <-c.stop:return // terminate go-routine.}}
}

执行流程如下:

  1. 调度器刚启动时,先计算所有任务的下次执行时间;
  2. 然后在一个for循环中,按照执行时间从早到晚排序,取出最近需要执行任务的时间点;
  3. 在select语句中等待到这个时间点,启动新的 goroutine 执行到期的任务,每个任务一个新的 goroutine;
  4. 如果在等待的过程中,又添加了新的任务(通过通道c.add),计算这个新任务的首次执行时间。跳到步骤 2,因为新添加的任务可能最早执行。

有几个细节需要注意一下:

  1. 任务到期判断使用的是本地时间:time.Now().Local();
  2. 如果没有任务,等待时间设置为now.AddDate(15, 0, 0),即 15 年,防止 CPU 空转;
  3. 任务都是在独立的 goroutine 中执行的;
  4. 通过实现sort.Interface接口可以实现自定义排序(代码中的byTime)。

最后,我们来看一下时间策略的代码。我们知道在Entry结构中存储了一个gron.Schedule类型的对象,调用该对象的Next()方法返回下次执行的时间点:

// src/github.com/roylee0704/gron/schedule.go
type Schedule interface {Next(t time.Time) time.Time
}

gron内置实现了两种Schedule,一种是periodicSchedule,即周期触发,gron.Every()函数返回的就是这个对象:

// src/github.com/roylee0704/gron/schedule.go
type periodicSchedule struct {period time.Duration
}

一种是固定时刻的周期触发,它实际上也是周期触发,只是固定了时间点:

type atSchedule struct {period time.Durationhh     intmm     int
}

他们的核心逻辑在Next()方法中,periodicSchedule只需要用当前时间加上周期即可得到下次触发时间。这里Truncate()方法截掉了当前时间中小于 1s 的部分:

func (ps periodicSchedule) Next(t time.Time) time.Time {return t.Truncate(time.Second).Add(ps.period)
}

atSchedule的Next()方法先计算当天该时间点,再加上周期就是下次触发的时间:

func (as atSchedule) reset(t time.Time) time.Time {return time.Date(t.Year(), t.Month(), t.Day(), as.hh, as.mm, 0, 0, time.UTC)
}func (as atSchedule) Next(t time.Time) time.Time {next := as.reset(t)if t.After(next) {return next.Add(as.period)}return next
}

periodicSchedule提供了At()方法可以转为atSchedule:

func (ps periodicSchedule) At(t string) Schedule {if ps.period < xtime.Day {panic("period must be at least in days")}// parse t naivelyh, m, err := parse(t)if err != nil {panic(err.Error())}return &atSchedule{period: ps.period,hh:     h,mm:     m,}
}

自定义时间策略

我们可以很轻松的实现一个自定义的时间策略。例如,我们要实现一个“指数退避”的时间序列,先等待 1s,然后 2s、4s…

type ExponentialBackOffSchedule struct {last int
}func (e *ExponentialBackOffSchedule) Next(t time.Time) time.Time {interval := time.Duration(math.Pow(2.0, float64(e.last))) * time.Seconde.last += 1return t.Truncate(time.Second).Add(interval)
}func main() {var wg sync.WaitGroupwg.Add(1)c := gron.New()c.AddFunc(&ExponentialBackOffSchedule{}, func() {fmt.Println(time.Now().Local().Format("2006-01-02 15:04:05"), "hello")})c.Start()wg.Wait()
}

运行结果如下:

2020-04-20 23:47:11 hello
2020-04-20 23:47:13 hello
2020-04-20 23:47:17 hello
2020-04-20 23:47:25 hello

第二次输出与第一次相差 2s,第三次与第二次相差 4s,第4次与第三次相差 8s,完美!

总结

本文介绍了gron这个小巧的定时任务库,如何使用,如何自定义任务和时间策略,顺带分析了一下源码。gron源码实现非常简洁,非常推荐阅读!

大家如果发现好玩、好用的 Go 语言库,欢迎到 Go 每日一库 GitHub 上提交 issue

go中的定时任务--gron相关推荐

  1. android中的定时任务一般有两种机制,android 定时任务

    使用timertask进行定时任务 首先创建TimerTask: class SynchroTimerTask extends TimerTask { @Override public void ru ...

  2. quartz mysql索引_分布式系统中的定时任务全解(二)

    在实际项目中,通常需要用到定时任务(定时作业),spring框架提供了很好的实现. 1.  下载spring-quartz插件包 这里默认当前系统中是集成了spring框架的基本功能的.去网上下载sp ...

  3. Spring Boot:在Spring Boot中使用定时任务

    2019独角兽企业重金招聘Python工程师标准>>> 本文主要介绍如何在Spring Boot中使用定时任务,假设你已经建好了一个基础的Spring Boot项目.首先,我们在项目 ...

  4. SpringBoot中的定时任务的同步与异步

    SpringBoot中的定时任务的同步与异步你确定真的知道? 授人以渔 Java领域;架构知识;面试心得;互联网行业最新资讯 定时任务调度功能在我们的开发中是非常常见的,随便举几个例子:定时清除一些过 ...

  5. 用crontab、crond在嵌入式系统中添加定时任务

    在嵌入式系统中,定时任务通过crond和cronttab两个系统命令来联合执行. 其中crond是定时任务的守护进程,系统开始时是没有开启的.crontab主要作用是管理用户的crontab file ...

  6. Linux中使用定时任务每周定时清理45天以前日志

    如题所示,生产服务器每天会产生很大的日志文件,为了不使硬盘被日志文件塞满,因此需要定期清理日志文件.这时我们可以写一个shell脚本用来清理某个路径下45天以前的日志,然后再设置一个定时任务每周定时执 ...

  7. 如何在linux系统中设置定时任务?

    1. 引入 1.1 在日常开发中,我们经常需要设置一些定时任务. 举个栗子:在进行Java Web开发时,通常我们采用Springboot 框架,我们可以通过 注解+ cron表达式,设置定时任务. ...

  8. java中写定时任务

    java中得定时任务,7种使用方式 1.使用普通thread实现 @Testpublic void test1() { // 单位: 毫秒final long timeInterval = 1000; ...

  9. Oracle 中的定时任务

    一.Oracle 中的定时任务的实例 1.1.创建一个测试表,只有一个 DATE 类型字段 CREATE TABLE TEST_A(TEST_ADD_DATA DATE); 1.2.创建一个自定义存储 ...

最新文章

  1. ubuntu安装mysql报错_在Ubuntu上安装mysql数据库和遇到的问题
  2. Axure之全局变量
  3. VirtualBox Network设置的NAT和Bridged Adapter模式区别
  4. 疯子的算法总结(六) 复杂排序算法 ① 归并排序 merge_sort()
  5. Codeforces Round #646 (Div. 2) E(贪心,bfs)
  6. IO流——字节流的使用
  7. mysql rr gap nextkey_mysql中的各种锁把我搞糊涂啦~
  8. .Net Framework学习的10个建议
  9. 因未能提交年度报告 瑞幸咖啡收到纳斯达克退市通知
  10. 详解java定时任务
  11. 思维导图学习---数据库相关基础思维导图(2)
  12. unable to resolve column. This inspection performs unresolved sql references check.
  13. uni-app 生成邀请二维码海报
  14. 价值几百元的EMlog仿大表哥资源网模版
  15. Matlab多重积分的两种实现【从六重积分到一百重积分】
  16. form表单同时提交带文本和图片的数据
  17. ASO优化在大数据时代应该怎么操作,aso优化如何操作
  18. IDM下载器免费高质量的Win下载工具无使用限制
  19. 计算机考研数学一用哪些书,2019计算机考研数学:常见三类参考书的使用方法...
  20. Android IPC机制之IPC概念、Android 多进程和相关基础知识

热门文章

  1. C#使用post方式提交json数据
  2. 黄聪:超实用的PHPExcel[导入][导出]实现方法总结
  3. Ts/Typescript基础运用
  4. No application 'meetme' for extension 错误
  5. 2020河南会考计算机成绩查询,2020河南中考成绩查询入口【已开通】
  6. 将Windows7系统改造为Linux(Centos7)系统
  7. MIS通用管理组件_权限配置
  8. python——判断一段音频是否有声音
  9. 1327: 五级制成绩(C语言)
  10. log4net配置文件说明