C# 计时器回调每24小时触发一次-DST处理是否正确?

C# 计时器回调每24小时触发一次-DST处理是否正确?,c#,service,timer,dst,C#,Service,Timer,Dst,我只是在想我解决每24小时运行一个任务的服务的方法,以及DST可能会如何伤害它 为了每天运行任务,我使用了System.Threading.Timer,周期为24小时,如下所示: _timer = new System.Threading.Timer(TimerCallback, null, requiredTime - DateTime.Now, new TimeSpan(24, 0, 0)); DateTime today = DateTime.Today; // tod

我只是在想我解决每24小时运行一个任务的服务的方法,以及DST可能会如何伤害它

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

_timer = new System.Threading.Timer(TimerCallback, null,
    requiredTime - DateTime.Now, new TimeSpan(24, 0, 0));
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
突然想到夏令时修正,我有三个想法:

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

我的第二个想法正确吗?如果是,我可以做些什么来避免这个问题?如果发生DST更正,则该任务不能在一小时后或一小时前运行。

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

现在,我使用
SystemEvents.TimeChanged
在系统时间更改时获取事件(意外),但是,此事件需要一个类似表单的消息循环

这个问题的答案帮助我在Windows服务中实现了消息循环:

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

如果有人不需要这样的消息循环就能得到答案

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小时。

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
您可能需要通过以下方式收集您的输入:

_timer = new System.Threading.Timer(TimerCallback, null,
    requiredTime - DateTime.Now, new TimeSpan(24, 0, 0));
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回退转换期间触发两次

如果所有这些听起来都太复杂,那么您可以尝试一个预构建的解决方案,例如。我