英文:
Golang time.Ticker to tick on clock times
问题
我正在处理一个Go程序,它要求我在(相当)精确的时钟时间运行某些函数(例如,每5分钟运行一次,但具体在3:00、3:05、3:10等时刻运行,而不仅仅是在程序启动后的每5分钟运行一次)。
在来这里寻求帮助之前,我尝试实现了一个定时器来实现这个功能,尽管它似乎工作得还可以,但感觉有点不太干净/巧妙,并且不是非常精确(只有几毫秒的偏差),但我想知道是否有理由相信这种差异会随时间增加而增加。
下面是我目前的实现方式,我真正想问的是,是否有更好的解决方案来实现我想要的功能(并且我可以更有信心)?
type ScheduledTicker struct {
C chan time.Time
}
// NewScheduledTicker返回一个在每小时后定义的间隔上进行滴答的定时器
// 例如,间隔为5分钟,偏移为0的定时器将在0:00:00、0:05:00 ... 23:55:00进行滴答
// 使用相同的间隔,但偏移为2分钟的定时器将在0:02:00、0:07:00 ... 23:57进行滴答
func NewScheduledTicker(interval time.Duration, offset time.Duration) *ScheduledTicker {
s := &ScheduledTicker{
C: make(chan time.Time),
}
go func() {
now := time.Now()
// 确定第一次滴答应该发生的时间
firstTick := now.Truncate(interval).Add(interval).Add(offset)
// 阻塞直到第一次滴答
<-time.After(firstTick.Sub(now))
t := time.NewTicker(interval)
// 发送初始滴答
s.C <- firstTick
for {
// 将原生time.Ticker的滴答转发到ScheduledTicker通道
s.C <- <-t.C
}
}()
return s
}
英文:
I am working on a Go program and it requires me to run certain function at (fairly) exact clock times (for example, every 5 minutes, but then specifically at 3:00, 3:05, 3:10, etc, not just every 5 minutes after the start of the program).
Before coming here and requesting your help, I tried implementing a ticker does that, and even though it seems to work ok-ish, it feels a little dirty/hacky and it's not super exact (it's only fractions of milliseconds off, but I'm wondering if there's reason to believe that discrepancy increases over time).
My current implementation is below, and what I'm really asking is, is there a better solution to achieve what I'm trying to achieve (and that I can have a little more confidence in)?
type ScheduledTicker struct {
C chan time.Time
}
// NewScheduledTicker returns a ticker that ticks on defined intervals after the hour
// For example, a ticker with an interval of 5 minutes and an offset of 0 will tick at 0:00:00, 0:05:00 ... 23:55:00
// Using the same interval, but an offset of 2 minutes will tick at 0:02:00, 0:07:00 ... 23:57
func NewScheduledTicker(interval time.Duration, offset time.Duration) *ScheduledTicker {
s := &ScheduledTicker{
C: make(chan time.Time),
}
go func() {
now := time.Now()
// Figure out when the first tick should happen
firstTick := now.Truncate(interval).Add(interval).Add(offset)
// Block until the first tick
<-time.After(firstTick.Sub(now))
t := time.NewTicker(interval)
// Send initial tick
s.C <- firstTick
for {
// Forward ticks from the native time.Ticker to the ScheduledTicker channel
s.C <- <-t.C
}
}()
return s
}
答案1
得分: 1
大多数平台上的计时器 API 都是基于系统时间而不是挂钟时间工作的。你所表达的是要有一个挂钟间隔。
正如其他答案所提到的,有一些开源的包可供使用。快速在谷歌上搜索“Golang Wall Clock ticker”会得到一些有趣的结果。
还有一件事需要考虑。在 Windows 上有“计划任务”,在 Linux 上有“cron 任务”,它们可以帮助你实现挂钟唤醒间隔。如果你的程序只需要在所需的间隔之间睡眠/触发,那么考虑使用这些功能。
但是如果你自己构建...
尝试在挂钟间隔上完成任务会受到桌面 PC 在笔记本盖子关闭时进入睡眠(暂停系统时间)以及系统时间和挂钟之间的时钟偏差的影响。有时用户会更改他们的 PC 的时钟 - 你可能会醒来并轮询 time.Now,发现你还停留在昨天!这在云中运行的服务器上不太可能发生,但在个人设备上是真实存在的。
在我的产品团队中,当我们真正需要挂钟时间或需要在跨越一个小时以上的间隔上执行某些操作时,我们会以更频繁的间隔唤醒来查看“是否到达指定时间”。例如,如果我们想每隔 12 小时执行一次某个操作,我们可能会每小时唤醒并轮询时间。(我们在工作中使用的是 C++ 而不是 Go)。
否则,对于 5 分钟的间隔,我的一般算法是睡眠(或设置系统计时器)1 分钟或更短时间。每次从 time.Sleep 返回后,检查当前时间(time.Now()
)是否在或之后的下一个预期间隔时间。发布你的通道事件,然后重新计算下一次唤醒时间。如果你提前醒来,甚至可以改变睡眠时间的粒度。
但要小心!golang 的 Time 对象包含挂钟时间和系统时钟时间。这包括 Time.Now()
返回的结果。Time.Add()
、Time.Sub()
和其他一些 Time 比较函数在使用单调时间之前会先使用挂钟时间。你可以通过以下方式去除 Time.Now
结果中的单调时间:
func GetWallclockNow() time.Time {
var t time.Time = time.Now()
return time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), t.Location())
}
然后,后续的 Add
和 After
操作将在挂钟空间中进行。
英文:
Most timer apis across all platforms work in terms of system time instead of wall clock time. What you are expressing to is have a wall clock interval.
As the other answer expressed, there are open source packages available. A quick Google search for "Golang Wall Clock ticker" yields interesting results.
Another thing to consider. On Windows there are "scheduled tasks" and on Linux there are "cronjobs" that will do the wall clock wakeup interval for you. Consider using that if all your program is going to do is sleep/tick between needed intervals before doing needed work.
But if you build it yourself...
Trying to get things done on wall clock intervals is complicated by desktop PCs going to sleep when laptop lids close (suspending system time) and clock skew between system and wall clocks. And sometimes users like to change their PC's clocks - you could wake up and poll time.Now and discover you're at yesterday! This is unlikely to happen on servers running in the cloud, but a real thing on personal devices.
On my product team, when we really need want clock time or need to do something on intervals that span more than an hour, we'll wake up at a more frequent interval to see if "it's time yet". For example, if there's something we want to execute every 12 hours, we might wake up and poll the time every hour. (We use C++ where I work instead of Go).
Otherwise, my general algorithm for a 5 minute interval would be to sleep (or set a system timer) for 1 minute or shorter. After every return from time.Sleep, check the current time (time.Now()
) to see if the current time is at or after the next expected interval time. Post your channel event and then recompute the next wake up time. You can even change the granularity of your sleep time if you woke up spuriously early.
But be careful! the golang Time object contains both a wall clock and system clock time. This includes the result returned by Time.Now()
. Time.Add()
, Time.Sub()
, and some of the other Time comparison functions work on the monotonic time first before falling over to wall clock time. You can strip the monotonic time out of the Time.Now
result by doing this:
func GetWallclockNow() time.Time {
var t time.Time = time.Now()
return time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), t.Location())
}
Then subsequent operations like Add
and After
will be in wall clock space.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论