golang的timer的一些坑
需求
最近项目有一些定时任务的需求,如每天的早上10:00:00定时的去执行一些任务。
问题
但是我遇到了一些问题,就是我定时10:00:00去执行,但是这个定时器疯狂的翻滚停不下来了,代码如下
package main
import (
"fmt"
"time"
)
func main() {
//每天10:05 通报昨日报警数量统计
go func() {
for {
// 定时器会一直执行
now := time.Now()
// 计算明天的时间
next := now.Add(time.Hour * 24 * time.Duration(0))
// 10:00 进行提醒
next = time.Date(next.Year(), next.Month(), next.Day(), 10, 0, 0, 0, next.Location())
fmt.Println("now:", next)
fmt.Println("next:", next)
fmt.Println("next.Sub(now:", next.Sub(now))
timer := time.NewTimer(next.Sub(now))
select {
case ts := <-timer.C:
fmt.Println("Start YesterdayAlarmStatistics ts=%s", ts.String())
}
}
}()
for {
}
}
控制台打印
Start YesterdayAlarmStatistics ts=%s 2022-04-02 14:49:34.505853 +0800 CST m=+1.806330418
now: 2022-04-02 10:00:00 +0800 CST
next: 2022-04-02 10:00:00 +0800 CST
next.Sub(now: -4h49m34.505858s
Start YesterdayAlarmStatistics ts=%s 2022-04-02 14:49:34.505868 +0800 CST m=+1.806345209
now: 2022-04-02 10:00:00 +0800 CST
next: 2022-04-02 10:00:00 +0800 CST
next.Sub(now: -4h49m34.505872s
Start YesterdayAlarmStatistics ts=%s 2022-04-02 14:49:34.505882 +0800 CST m=+1.806359626
now: 2022-04-02 10:00:00 +0800 CST
next: 2022-04-02 10:00:00 +0800 CST...
它会疯狂的打印并不会停下来,可以看到下面这行代码里传递的值其实是个负数-4h49m34.505872s,但是定时器仍在执行,百思不得其姐
timer := time.NewTimer(next.Sub(now))
分析:
Golang 的NewTimer方法调用后,生成的timer会放入最小堆,一个后台goroutine会扫描这个堆,将到时的timer进行回调和channel。
而golang的timer的Stop方法, 是只负责把timer从堆里移除,不负责close 上面的channel。于是第一次执行完毕后,之后所有值我们传递的都是个负数,但是stop并没有关闭channel,导致代码仍在在运行,就是个无限for循环。
解决方案1:单次定时器
缺点:下面的方法会让定时器变为单次执行的定时器
package main
import (
"fmt"
"time"
)
func main() {
//每天10:05 通报昨日报警数量统计
go func() {
for {
// 定时器会一直执行
now := time.Now()
// 计算明天的时间
next := now.Add(time.Hour * 24 * time.Duration(0))
// 10:00 进行提醒
next = time.Date(next.Year(), next.Month(), next.Day(), 14, 32, 10, 10, next.Location())
fmt.Println("now:", next)
fmt.Println("next:", next)
fmt.Println("next.Sub(now:", next.Sub(now))
timer := time.NewTimer(next.Sub(now))
select {
case ts := <-timer.C:
fmt.Println("Start YesterdayAlarmStatistics ts=%s", ts.String())
b := timer.Stop()
if !b {
fmt.Println("==============")
ts := <-timer.C
fmt.Println(ts)
}
}
}
}()
for {
}
}
解决方案2:每天10:05:00执行
其实是我代码里埋的坑,既然它会一直执行,但是执行时间我们总能确定吧?所以我们在本次执行之后,修改下次的执行时间就行了
go func() {
for {
now := time.Now()
var next time.Time
//每天10:05发送,
//如果当前时间是10:05之前那么按当前时间生成定时器,
//如果不是则说明该时间已经过了10:05,于是明天发送,按照明天的10:05生成定时器
if now.Hour() < 10 || now.Hour() == 10 && now.Minute() < 5 {
next = now
} else {
next = now.Add(time.Hour * 24)
}
//10:05:00 进行提醒
next = time.Date(next.Year(), next.Month(), next.Day(), 10, 5, 0, 0, next.Location())
logger.Info("YesterdayAlarmStatistics Now=%s Next=%s", now, next)
timer := time.NewTimer(next.Sub(now))
select {
case ts := <-timer.C:
go yourFunction()
logger.Info("StartYesterdayAlarmStatistics ts=%s", ts.String())
time.Sleep(120 * time.Second)
timer.Stop()
}
}
}()
扩展:每周一的10点执行
go func() {
for {
now := time.Now()
nowWeekDay := int(now.Weekday())
nowHour := now.Hour()
//Current Date : 0,1,2,3,4,5,6
//Next Monday : 1,7,6,5,4,3,2
addDay := (8 - nowWeekDay) % 7 //进程可能在任何时候重启
//这类场景无需特殊处理,上面的公式得到的addDay==0
/*if nowWeekDay == 1 && nowHour < 10 { //nowHour 小于 10 说明本周的排班还未更新,需要处理一下
addDay = 0
}*/
if nowWeekDay == 1 && nowHour >= 10 { //nowHour 大于 10 说明本周的排班已更新,准备处理下周的即可
addDay = 7
}
// 计算明天的时间
next := now.Add(time.Hour * 24 * time.Duration(addDay))
// 10:00 进行提醒
next = time.Date(next.Year(), next.Month(), next.Day(), 10, 0, 0, 0, next.Location())
// 测试环境,每天
// timer := time.NewTimer(24 * 60 * 60 * time.Second)
// 正式环境,每周一
timer := time.NewTimer(next.Sub(now))
select {
case ts := <-timer.C:
go UpdateOnduty() //目前只处理这个租户
logger.Info("Start UpdateOnduty_7day ts=%s", ts.String())
timer.Stop()
}
}
}()
参考链接:https://www.cnblogs.com/jiangz222/p/11622495.html