如何定义特定DateTime触发的事件?优化现有F#实现方案咨询
问题描述
目标是创建一个在特定日期时间条件满足时触发的事件——例如当DateTime.Now的秒数变化时(后续需支持月份变化)。
尝试的实现代码:
type Timer() = let timerEvent = new Event<System.DateTime>() let cts = new System.Threading.CancellationTokenSource() member this.onSecondTick = let loop = async { while true do let time = System.DateTime.Now let tickDecrTime = time - System.TimeSpan(1) if time.Second <> tickDecrTime.Second then timerEvent.Trigger(time) else let offset = 200; let sleepTime = 1000 - time.Millisecond - offset; if sleepTime > 0 then do! Async.Sleep(sleepTime) } Async.Start(loop, cts.Token) timerEvent.Publish interface System.IDisposable with member this.Dispose() = printfn "Disposed" cts.Cancel() member this.Dispose() =( this:> System.IDisposable).Dispose()
遇到的问题:
- 若不在
async{}的else分支中使用Async.Sleep,在FSI执行时.NET Host进程CPU占用率会升至20%(设备为Core i5),不合理。 - 若使用
Async.Sleep,则会因设置的偏移毫秒值导致偶尔错过秒数变化事件。
询问:是否存在比在无限循环中检查DateTime.Now条件更优的日期时间相关事件触发方式?
解决方案
有两种更优的实现方式,可避免轮询带来的CPU占用或事件丢失问题:
方式一:使用System.Timers.Timer精准触发
适合固定间隔(如每秒)的场景,后续扩展到月份变化时可动态调整定时器间隔:
type DateTimeTrigger() = let timerEvent = Event<System.DateTime>() let timer = new System.Timers.Timer() let adjustTimerForNextTick() = let now = System.DateTime.Now // 计算到下一秒开始的毫秒数 let msToNextSecond = 1000 - now.Millisecond timer.Interval <- float msToNextSecond let onTimerElapsed _ _ = let now = System.DateTime.Now timerEvent.Trigger(now) adjustTimerForNextTick() do timer.Elapsed.AddHandler(onTimerElapsed) adjustTimerForNextTick() timer.Start() member this.OnTick = timerEvent.Publish interface System.IDisposable with member this.Dispose() = timer.Stop() timer.Dispose() printfn "Disposed"
- 原理:每次触发后重新计算到下一个目标时间点的间隔,避免固定间隔累积误差,也不会因偏移导致错过事件。
- 扩展到月份变化:修改
adjustTimerForNextTick,计算当前时间到下一个月第一天0点的毫秒数,设置为定时器间隔即可。
方式二:基于异步等待精准计算的延迟
无需依赖定时器,直接通过异步等待计算好的延迟时间,灵活性更高:
type DateTimeTrigger() = let timerEvent = Event<System.DateTime>() let cts = System.Threading.CancellationTokenSource() let rec loop() = async { let now = System.DateTime.Now // 计算到下一秒的延迟 let delayMs = 1000 - now.Millisecond do! Async.Sleep(delayMs) let triggerTime = System.DateTime.Now timerEvent.Trigger(triggerTime) return! loop() } do Async.Start(loop(), cts.Token) member this.OnTick = timerEvent.Publish interface System.IDisposable with member this.Dispose() = cts.Cancel() printfn "Disposed"
- 原理:每次循环先计算到下一个目标时间的准确延迟,等待后触发事件,完全避免轮询的CPU消耗,也不会错过事件。
- 扩展到月份变化:将延迟计算改为
nextMonthStart - now的毫秒数即可,注意处理时间跨度的转换。
核心优势
两种方式都避免了无限轮询的CPU浪费,同时通过精准计算下一个触发点的延迟,确保触发时机准确,无需设置偏移值。
内容的提问来源于stack exchange,提问作者Konst Sh
相关产品推荐
相关产品推荐

