Go定时任务实现

定时任务调度是常见的场景,从简单点本地任务调度,到分布式定时任务调度,被广泛的使用。本文汇总了 awesome-go 里全部的本地定时任务库,并横向对比其实现原理,以及使用场景和优缺点,欢迎收藏随时参考。

本文收纳的本地定时任务库如下:

  1. onatm/clockwerk
  2. withself/clockwork
  3. kr/go-cron
  4. roylee0704/gron
  5. carlescere/scheduler

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()
}

 

使用模式可以总结为如下步骤其他开源库也基本上都是按照这个模式使用。
  1. New 创建一个调度对象
  2. Every 确定的 duration
  3. Do 将任务加入调度对象
  4. Start 启动执行调度对象里的任务

源码非常简洁,直接上流程图:

image.png
image.png

其核心控制逻辑是:

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 触发执行

返回任务对象,可控制停止

综合我们可以总结出来,核心流程:

posted @ 2023-11-02 15:42  韩梦芫  阅读(142)  评论(0编辑  收藏  举报