Go定时任务实现
定时任务调度是常见的场景,从简单点本地任务调度,到分布式定时任务调度,被广泛的使用。本文汇总了 awesome-go 里全部的本地定时任务库,并横向对比其实现原理,以及使用场景和优缺点,欢迎收藏随时参考。
本文收纳的本地定时任务库如下:
1. 定时任务基础
最基础的定时任务
众所周知,go 语言的 time 库提供了 Ticker 方法,可以通过:
ticker := time.NewTicker(duration) 的方式获取到一个定时返回的 chan,此可以用来帮助我们实现基础的定时任务功能,比如如下函数就是基础的实现: package main import ( "fmt" "time" ) func NewCronJob(duration time.Duration, job func()) (stopChan chan bool) { ticker := time.NewTicker(duration) stopChan = make(chan bool) go func(ticker *time.Ticker) { defer ticker.Stop() // 由于ticker.Stop()内部不会关闭chan,故使用 for range 会内存泄露 // 推荐使用 for + select + return 的方式,让 ticker 最终被GC for { select { case <-ticker.C: job() case stop := <-stopChan: if stop { close(stopChan) return } } } }(ticker) return stopChan } func main() { stopCtr := NewCronJob(2*time.Second, func() { fmt.Printf("hello word:%+v\n", time.Now()) }) time.Sleep(10 * time.Second) stopCtr <- true // example output: // hello word:2023-02-26 11:10:17.470147 +0800 CST m=+2.001352251 // hello word:2023-02-26 11:10:19.470141 +0800 CST m=+4.001330084 // hello word:2023-02-26 11:10:21.470154 +0800 CST m=+6.001327917 // hello word:2023-02-26 11:10:23.469824 +0800 CST m=+8.000981501 // hello word:2023-02-26 11:10:25.470273 +0800 CST m=+10.001414209 }
使用 time.Sleep()
也可以实现这个功能,但是 Ticker 会更加优雅些。
cron 表达式
time.Ticker()
非常简单好用,但是也有不足,就是难以控制让任务在准确地时间里执行,比如 ticker 可以实现每半个小时执行一次,但是无法直接实现,每个小时 30 分时执行一次。
Linux 系统里的 crontab 可以完美解决这个问题,通过类似如下的字符串,就定义了在每个小时 30 分执行的任务。
30 * * * *
corntab 只有五位
* * * * *
- - - - -
| | | | |
| | | | +----- 星期几 (0 - 6) (星期天=0)
| | | +---------- 月份 (1 - 12)
| | +--------------- 一个月中的第几天 (1 - 31)
| +-------------------- 小时 (0 - 23)
+------------------------- 分钟 (0 - 59)
基础 corntab 派生的 corn 有 6 位或者 7 位,多出的一到两位,精确到了秒或者年,具体可参考此文。
像开源的 robfig/cron 就是 Go 的知名开源corn库。
2. 定时任务通用实现——基础
如果将上述定时任务的实例代码进行封装,就可以实现一个简单由实用的定时任务。下文将分析并对比,awesome-go 推荐的定时任务库。
经典样例——onatm/clockwerk
先看下使用样例:
type DummyJob struct{} func (d DummyJob) Run() { fmt.Println("Every 30 seconds") } func main() { var job DummyJob c := clockwerk.New() c.Every(30 * time.Second).Do(job) c.Start() }
使用模式可以总结为如下步骤其他开源库也基本上都是按照这个模式使用。
- New 创建一个调度对象
- Every 确定的 duration
- Do 将任务加入调度对象
- Start 启动执行调度对象里的任务
源码非常简洁,直接上流程图:
其核心控制逻辑是:
func (c *Clockwerk) run() { ticker := time.NewTicker(100 * time.Millisecond) go func() { for { select { case <-ticker.C: // 遍历所有的job,执行到点的任务 c.runPending() continue case <-c.stop: ticker.Stop() return } } }() }
其他开源库对比
项目 |
最近更新时间 |
定时声明模式 |
定时机制 |
能否中途添加任务 |
管理任务能力 |
panic 相关 |
---|---|---|---|---|---|---|
onatm/clockwerk |
2019 |
时间间隔 time. Duration |
100 ms 的 ticker 控制轮询 |
否 |
可停止全部任务 |
任务 panic 后,会 recover 并重新调度 |
whiteshtef/clockwork |
2020 |
Every 模式 |
死循环默认 sleep 333 ms(可设置) |
可 |
不可停止 |
参数声明错误 panic,任务不处理 |
rk/go-cron |
2013 |
corn 表达式 |
死循环默认 sleep 1 s(不可设置) |
可 |
不可停止 |
无 |
roylee 0704/gron |
2016 |
Every 模式 |
由 time. After 触发执行 |
可 |
可停止全部任务 |
参数声明错误 panic,任务不处理 |
carlescere/scheduler |
2015 |
Every 模式 |
由 time. After 触发执行 |
可 |
返回任务对象,可控制停止 |
无 |
综合我们可以总结出来,核心流程: