代码之家  ›  专栏  ›  技术社区  ›  Ray Brian Agnew

计时器每24小时回调一次-DST处理正确吗?

  •  4
  • Ray Brian Agnew  · 技术社区  · 11 年前

    我只是想了一下我解决一项每24小时运行一次任务的服务的方法,以及夏令时可能会对它造成什么影响。

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

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

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

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

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

    3 回复  |  直到 11 年前
        1
  •  2
  •   Matt Johnson-Pint    11 年前

    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
    

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

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

    另一个想法是稍微改变一下。不进行短轮询,而是对列表进行排序,并设置一个延迟时间,直到下一个事件,最大延迟时间(可能是一个小时)。同样,当计时器启动时,您可以查看是否有什么事情要做,或者是否需要设置另一个计时器延迟。您必须在应用程序启动时以及计划新事件时运行此程序。

    对于这两种方法中的任何一种,您都应该使用 日期时间偏移 ,或等效的UTC 日期时间 。否则,您可能会在错误的夏令时时间开火,甚至开枪 两次 在DST回退过渡期间。

    如果所有这些听起来太复杂,那么您可以尝试一个预先构建的解决方案,例如 Quartz.net 。特别是,请阅读 this section of their FAQ .

    关于你的第一个和第三个要点,我完全同意——但这永远不会发生。即使发生了,我们仍然需要考虑它发生的多年历史。如果你还没看 this video 已经,你应该。

        2
  •  1
  •   Hans Passant    11 年前

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

    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也有同样的问题。

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

        3
  •  0
  •   Community Egal    7 年前

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

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

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

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