计时器回调每 24 小时引发一次 - DST 处理是否正确

本文关键字:一次 DST 处理 是否 回调 小时 计时器 | 更新日期: 2023-09-27 18:32:57

我只是想到了我解决每 24 小时运行一次任务的服务的方式,以及 DST 如何可能伤害它。

为了每天运行任务,我使用了周期为 24 小时的 System.Threading.Timer ,如下所示:

_timer = new System.Threading.Timer(TimerCallback, null,
    requiredTime - DateTime.Now, new TimeSpan(24, 0, 0));

突然想到夏令时校正,我有三个想法:

  • DST是无用的,我们应该摆脱它。
  • 计时器是否正确处理此问题?我认为不是 - 它只是等待 24 小时 - 无论时钟是否改变。它只是等待指定的时间段,然后再次调用 TimerCallback。
  • DST是无用的,我们真的应该摆脱它。

我的第二个想法正确吗?如果是,我该怎么做才能避免此问题?如果发生了 DST 更正,则不得晚一小时或提前运行任务。

计时器回调每 24 小时引发一次 - DST 处理是否正确

SystemEvents.TimeChanged会告诉你时钟是否已被用户更改。 它不会随着时钟的每次滴答声而定期触发。 因此,它对于计划事件没有用,除了您可以在事件发生时重新计算计时器。 (我认为如果系统与时间服务器同步,它也会触发,但我对此并不乐观。

如果你试图按照汉斯的建议计算墙上时间的差异,要小心。 你不能只使用DateTime. 观察:

// With time zone set for US Pacific time, there should only be 23 hours
// between these two points
DateTime a = new DateTime(2013, 03, 10, 0, 0, 0, DateTimeKind.Local);
DateTime b = new DateTime(2013, 03, 11, 0, 0, 0, DateTimeKind.Local);
TimeSpan t = b - a;
Debug.WriteLine(t.TotalHours);  // 24

即使指定了本地类型,它也不会考虑 DST。

如果要采用此方法,则必须使用DateTimeOffset类型。

DateTimeOffset a = new DateTimeOffset(2013, 03, 10, 0, 0, 0, TimeSpan.FromHours(-8));
DateTimeOffset b = new DateTimeOffset(2013, 03, 11, 0, 0, 0, TimeSpan.FromHours(-7));
TimeSpan t = b - a;
Debug.WriteLine(t.TotalHours);  // 23

您可能需要使用如下内容收集输入:

DateTime today = DateTime.Today;      // today at midnight
DateTime tomorrow = today.AddDays(1); // tomorrow at midnight
TimeZoneInfo tz = TimeZoneInfo.Local;
DateTimeOffset a = new DateTimeOffset(today, tz.GetUtcOffset(today));
DateTimeOffset b = new DateTimeOffset(tomorrow, tz.GetUtcOffset(tomorrow));
TimeSpan t = b - a;
Debug.WriteLine(t.TotalHours);  // 23, 24, or 25 depending on DST

但是 - 设置单个计时器运行这么长时间可能不是一个好主意。 不仅时钟可能会因用户或系统时间同步而更改,而且应用程序或系统可能会关闭或重新启动。 此外,如果您有许多这样的任务,则最终可能会消耗大量资源以闲置。

一个

想法是保留一个列表,以便下次触发事件。 在您的应用程序中,您将触发一个短暂的轮询计时器(例如每分钟左右一次(,并将当前时间与列表中的值进行比较,以了解是否确实需要执行任何操作。

另一个想法是略有不同。 与其进行简短的轮询,不如保持列表的排序,并设置延迟到下一个事件的时间,并有一些最大延迟(可能是一个小时(。 同样,当计时器触发时,您可以查看是否有任何操作,或者是否需要设置另一个计时器延迟。 您必须在应用程序启动时以及计划新事件时运行此命令。

使用这两种方法中的任何一种,都应使用 DateTimeOffset 或等效的 UTC DateTime 进行调度。 否则,您可能会在错误的 DST 触发时间触发 - 甚至在 DST 回退转换期间触发两次

如果所有这些听起来都太复杂,那么您可以尝试预先构建的解决方案,例如 Quartz.net。 特别是,请阅读其常见问题解答的这一部分。

关于你的第一个和第三个要点,我完全同意——但这永远不会发生。 即使它确实发生了,我们仍然必须考虑它确实发生的所有多年历史。 如果您还没有看过此视频,您应该观看。

如果有人在不需要这样的消息循环的情况下得到答案

SystemEvents 类已经提供了自己的隐藏窗口和调度程序循环(如果您自己不提供的话(。 它知道你有一个似乎很神奇,但它在 Windows 编程中有一个完善的合同。 它在你用来添加事件处理程序的线程上使用 Thread.GetApartmentState((。 它返回 STA,就像在任何 GUI 应用程序中一样,然后它信任您的程序实现 STA 协定并泵送消息循环,并且 SystemEvents 不执行任何特殊操作。

如果它返回 MTA,就像在控制台模式应用或服务中一样,则它假定您的程序没有调度程序循环并支持 MTA 承诺的线程并启动一个新线程。 您可以在调试器的"调试 + 窗口 + 线程"窗口中看到该线程,该线程的名称为".NET SystemEvents"。 该线程创建一个隐藏窗口并泵送一个循环,相当于 Application.Run((。 您也可以使用 Spy++ 看到该窗口,它的名称是".NET-BroadcastEventWindow.xxxx"。

值得注意的是,此行为是导致许多 GUI 程序严重失败的原因,通常是在 GUI 应用程序中的 UserPpreferencesChanged 事件上,通常是在用户解锁工作站时。 当程序在非 STA 的工作线程上创建初始屏幕时,会发生这种情况。 SystemEvents 假定程序需要帮助并创建该帮助程序线程。 现在,它会在错误的线程上触发事件,程序使用该线程来更新其 UI。 如果它是一个 STA 线程,但允许该线程退出,系统事件也会出错,系统会尝试在不再存在的线程上触发事件,如果失败,则会回退到 TP 线程。 这在GUI应用程序中非常非常糟糕,死锁是常见的结果。

但是,当然,在您的情况下,您非常喜欢SystemEvents类的工作方式,您确实想要该辅助线程,因此您不必自己编写它。 请记住,TimeChanged 事件在与服务启动的任何线程无关的完全任意线程上触发,因此当然需要正确的联锁。 顺便说一句,你自己的问题也有同样的问题。

请考虑简单的解决方案。 您只需要根据明天的挂钟时间和今天的时间之间的差异来计算计时器间隔。 换句话说,绝对时间,而不是增量。 如果跨越 DST 更改,这将产生 23 或 25 小时。

我找到了我的解决方案,尽管它可能不是Windows服务的最佳解决方案。

我现在使用 SystemEvents.TimeChanged 在系统时间更改时获取事件(惊喜(,但是,此事件需要类似于表单的消息循环。

这个问题的答案帮助我在 Windows 服务中实现了消息循环:https://stackoverflow.com/a/9807963/777985

如果有人在不需要这样的消息循环的情况下得到答案,我会接受的!