写点什么

Go 中更好的定时调度

用户头像
baiyutang
关注
发布于: 3 小时前
Go 中更好的定时调度

译者:baiyutang

原文:https://dev.to/stephenafamo/better-scheduling-in-go-4f7e


在很多场景中,你想在 Go 中能够调度函数。尽管有很多工具能够实现调度(比如 Cron),我更喜欢我程序中的整个工作都包含在相同的代码或二进制中,不需要去到系统中的 crontab 去发现什么正在进行。

幸好, time 标准库已经提供了很多调度事件的工具。让我们看看有哪些工具并什么时候可以用到。

time.After()

time.After() 允许我们在一段时间后指定一个动作。例如:

注意helper 常量 time.Second, time.Munitetime.Hour 都是 time.Duration 类型。所以,如果我们比如提供一个时长,我们可以用一个数字乘以这些常量,表达式会返回 time.Duration 类型。

package main
import ( "fmt" "time")
func main() { // This will block for 5 seconds and then return the current time theTime := <-time.After(time.Second * 5) fmt.Println(theTime.Format("2006-01-02 15:04:05"))}
复制代码


2019-09-22 09:33:05
复制代码

time.Ticker

time.After() 对于一次性动作是好的。但是 cron 作业的强大表现在重复动作。


所以,为了实现重复工作,我们用 time.Ticker 。在很多用例中我们可以使用函数 time.Tick() 创建一个断续器。例如:

package main
import ( "fmt" "time")
func main() { // This will print the time every 5 seconds for theTime := range time.Tick(time.Second * 5) { fmt.Println(theTime.Format("2006-01-02 15:04:05")) }}
复制代码


2019-09-22 10:07:542019-09-22 10:07:592019-09-22 10:08:042019-09-22 10:08:092019-09-22 10:08:14
复制代码

注意:当我们使用 Ticker,延迟后的第一个事件将会被触发。

使用 time.Tick() 的危险

当我们使用 time.Tick() 函数时,我们不能直接访问 time.Tick() 之后的,所以我们无法彻底的关闭。


如果我们不需要明确的停止断续器(比如断续器会永远的运行),那么这可能是一个问题。但是,我们简单的忽略断续器,资源将不会被 gc 而释放掉。


time.Tick() 的局限性

有几件事情,我们不能轻易的使用 time.Ticker

  1. 明确的开始时间。

  2. 停止断续器。

使用自定义函数扩展 time.Tick()

为了克服 time.Tick() 的限制,“我”创建了一个 helper 函数在“我”项目中用到的。

func cron(ctx context.Context, startTime time.Time, delay time.Duration) <-chan time.Time {    // Create the channel which we will return    stream := make(chan time.Time, 1)
// Calculating the first start time in the future // Need to check if the time is zero (e.g. if time.Time{} was used) if !startTime.IsZero() { diff := time.Until(startTime) if diff < 0 { total := diff - delay times := total / delay * -1
startTime = startTime.Add(times * delay) } }
// Run this in a goroutine, or our function will block until the first event go func() {
// Run the first event after it gets to the start time t := <-time.After(time.Until(startTime)) stream <- t
// Open a new ticker ticker := time.NewTicker(delay) // Make sure to stop the ticker when we're done defer ticker.Stop()
// Listen on both the ticker and the context done channel to know when to stop for { select { case t2 := <-ticker.C: stream <- t2 case <-ctx.Done(): close(stream) return } } }()
return stream}
复制代码

在这个函数中发生了什么

函数接收 3 个参数:

  1. Context,每当 context 被取消,断续器将会被停止。所以我们创建一个带有 cancel 函数 或 超时 或截止日期的 context。当 context 取消,函数能够优雅的释放资源。

  2. Time,开始时间作为参照可以知道什么时候开始计时。如果开始时间是未来,断续器会直到指定时间才开始。如果开始时间是过去,该函数通过添加适当的延迟倍数来计算将来发生的第一个事件。

  3. Duration,这是时间间隔,从开始时间开始计算。

使用自定义函数的例子

星期二下午 2 点运行

ctx := context.Background()
startTime, err := time.Parse( "2006-01-02 15:04:05", "2019-09-17 14:00:00",) // is a tuesdayif err != nil { panic(err)}
delay := time.Hour * 24 * 7 // 1 week
for t := range cron(ctx, startTime, delay) { // Perform action here log.Println(t.Format("2006-01-02 15:04:05"))}
复制代码

每个小时运行

ctx := context.Background()
startTime, err := time.Parse( "2006-01-02 15:04:05", "2019-09-17 14:00:00",) // any time in the past works but it should be on the hourif err != nil { panic(err)}
delay := time.Hour // 1 hour
for t := range cron(ctx, startTime, delay) { // Perform action here log.Println(t.Format("2006-01-02 15:04:05"))}
复制代码

每 10 分钟运行

ctx := context.Background()
startTime, err := time.Now().AddDate(0, 0, 7) // see https://golang.org/pkg/time/#Time.AddDateif err != nil { panic(err)}
delay := time.Minute * 10 // 10 minutes
for t := range cron(ctx, startTime, delay) { // Perform action here log.Println(t.Format("2006-01-02 15:04:05"))}
复制代码

总结

有了这个函数,我已经能在项目中更好的控制定时调度。希望,它也能对你有所帮助。


扩展

  1. 官方库 >> https://pkg.go.dev/time#Tick

发布于: 3 小时前阅读数: 12
用户头像

baiyutang

关注

广州 2017.12.13 加入

Microservices | Golang | Cloud Nitive | “Smart work,Not hard”

评论

发布
暂无评论
Golang 中更好的定时调度