定时器回调每24小时启动一次 - DST是否正确处理?

时间:2013-09-27 08:29:48

标签: c# service timer dst

我只是想到了解决每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更正发生,则任务不得在一小时或更早的时间运行。

3 个答案:

答案 0 :(得分:2)

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。请特别阅读this section of their FAQ

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

答案 1 :(得分: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事件会触发一个完全任意的线程,该线程与您的服务启动的任何线程无关,因此当然需要正确的互锁。你用自己的btw也有同样的问题。

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

答案 2 :(得分:0)

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

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

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

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