4

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

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

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

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

  • DST 没用,我们应该摆脱它。
  • Timer 是否正确处理这个问题?我认为不会——它只是等待 24 小时——不管时钟是否改变。它只是等待指定的时间段,然后再次调用 TimerCallback。
  • DST 没用,我们真的应该摆脱它。

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

4

3 回答 3

2

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

如果您尝试按照 Hans 的建议计算挂墙时间的差异,请小心。你不能只使用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

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

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

另一个想法将是一个轻微的变化。代替短时间的轮询,您将保持您的列表排序,并设置延迟到下一个事件的时间,并具有一些最大延迟(可能是一个小时)。同样,当计时器触发时,您会查看是否有任何事情要做,或者是否需要设置另一个计时器延迟。您必须在您的应用程序启动时运行它,并且任何时候安排新事件。

使用这些方法中的任何一种,您都应该使用 aDateTimeOffset或等效的 UTC来安排时间DateTime。否则,您可能会在 DST 的错误时间触发 - 甚至在 DST 回退过渡期间触发两次。

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

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

于 2013-09-29T21:02:05.077 回答
1

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

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

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

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

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

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

于 2013-09-27T10:16:51.887 回答
0

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

我现在用来SystemEvents.TimeChanged在系统时间更改时获取一个事件(惊喜),但是,这个事件需要一个消息循环,如 Form.

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

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

于 2013-09-27T09:32:42.013 回答