system . timer . timer在WPF应用程序中的行为,在Hibernate和Sleep之后

本文关键字:timer Hibernate 之后 Sleep WPF 应用程序 system | 更新日期: 2023-09-27 18:15:21

我使用的是System.Timers。定时器在我的WPF应用程序。我想了解定时器是如何表现的,在电脑休眠和睡眠之后。我的应用程序出现了一些奇怪的问题,电脑从休眠状态恢复。

我应该如何处理定时器,以及它们在计算机处于休眠/睡眠模式时的行为方式?

我有一个午夜计时器,它应该在每个午夜工作重置UI上的默认值。

下面是创建计时器的代码:
private void ResetMidnightTimer() 
        { 
            // kill the old timer
            DisposeMidnightTimer();
            _midnightTimer = new Timer();
            // scheduling the timer to elapse 1 minute after midnight
            _midnightTimer.Interval = (DateTime.Today.AddDays(1).AddMinutes(1) - DateTime.Now).TotalMilliseconds;
            _midnightTimer.Elapsed += (_, __) => UpdateRecommendedCollectingTime();
            _midnightTimer.Enabled = true;
            _midnightTimer.Start();
        }

在UI页面的构造函数中,我调用了调用ResestMidnightTimer()的方法,并创建了事实上的计时器。在那之后,计时器只是等待夜晚。

当夜间时间(实际上是12:01 AM)到来时,计时器工作,按预期重置默认值,然后处置现有计时器。最后,它为第二天创建一个新的午夜计时器。但是,如果我在那天尝试休眠电脑,午夜计时器将无法工作,也不会重置默认值。

是否因为在休眠时它只是将事件处理延迟了相同的休眠时间?

system . timer . timer在WPF应用程序中的行为,在Hibernate和Sleep之后

这取决于您如何使用计时器。如果你使用它们来启动一些不经常发生的事件(超过几分钟),那么你可能会看到一些"奇怪"的行为。因为你没有指定那个"奇怪"的行为是什么,我将假设你的程序的计时器比它应该的要晚。

说明:进入睡眠/休眠的问题是所有的程序都被挂起。这意味着你的计时器没有被更新,因此当你睡觉/休眠并回来时,就好像你在睡觉/休眠期间被冻结了一样。这意味着,如果你设置了一个小时的计时器,而你的电脑在15分钟的时候进入睡眠状态,一旦它醒来,它将再过45分钟,不管电脑睡了多长时间。

解决方案:一种修复方法是保留事件最后发生时间的DateTime。然后,让计时器定期(每10秒或每10分钟,取决于所需的精度)关闭,并检查最后一次执行的DateTime。如果当前执行时间与上次执行时间之间的差值大于或等于所需的时间间隔,则运行execution。

这将修复它,如果一个事件"应该"在睡眠/休眠期间发生,它将在你从睡眠/休眠返回的那一刻开始。

更新:上面提出的解决方案将工作,我将填写一些细节来帮助您实现它。

  • 不是创建/处置新的计时器,而是创建一个计时器来使用经常性的 (AutoReset属性设置为true)

  • 单个定时器的时间间隔NOT应根据下次事件发生的时间设置。相反,应该将其设置为您选择的值,该值将表示轮询频率(检查"事件"是否应该运行的频率)。这种选择应该在效率和精确度之间取得平衡。如果你需要它在接近12:01的时间运行,那么你可以将间隔设置为5-10秒左右。如果恰好在凌晨12:01分是不太重要的,您可以将间隔增加到1-10分钟。

  • 您需要保持最后一次执行发生的日期时间下一次执行应该发生的时间。我更喜欢'当下一次执行应该发生',这样你就不会做(LastExecutionTime + EventInterval)每次计时器结束时,你只是比较当前时间和事件应该发生的时间。

  • 一旦计时器过去,事件应该发生(大约12:01 AM),您应该更新存储的DateTime,然后运行您想要在12:01 AM运行的代码。

睡眠与休眠澄清:睡眠与休眠的主要区别在于,在睡眠中,所有内容都保存在RAM中,而休眠将当前状态保存到磁盘。休眠的主要优点是RAM不再需要电源,因此消耗更少的能量。这就是为什么在处理使用有限能量的笔记本电脑或其他设备时,建议使用休眠而不是睡眠。

也就是说,程序的执行没有区别,因为它们在两种情况下都被挂起。不幸的是,System.Timers.Timer不会"唤醒"计算机,所以你不能强制你的代码在凌晨12:01运行。

我相信还有其他的方法来"唤醒"电脑,但除非你走那条路,你能做的最好的是在你的计时器从睡眠/休眠中出来后的下一个"轮询事件"中运行你的"事件"。

这是因为在休眠时,它只是将事件处理延迟了与休眠时相同的时间?

当计算机处于挂起模式(即睡眠或休眠)时,它不做任何事情。这特别包括,处理唤醒监视计时器事件队列的线程的调度器没有运行,因此该线程在恢复执行以处理下一个计时器方面没有取得任何进展。

事件不是显式地延迟本身。但是,是的,这就是净效应。

在某些情况下,可以使用不存在此问题的计时器类。System.Windows.Forms.TimerSystem.Windows.Threading.DispatcherTimer都不是基于Windows线程调度器,而是基于WM_TIMER消息。由于这条消息的工作方式—它是在线程的消息循环检查消息队列时"动态地"生成的,基于计时器的过期时间是否已经过了&在某种程度上,它类似于您的问题的另一个答案中描述的轮询解决方案—它不受计算机被挂起所造成的延迟的影响。

您已经说明了您的场景涉及到一个WPF程序,因此您可能会发现您的最佳解决方案实际上是使用DispatcherTimer类,而不是System.Timers.Timer

如果你决定你需要一个不绑定到UI线程的计时器实现,这里有一个System.Threading.Timer版本,它将正确地考虑到挂起时花费的时间:

class SleepAwareTimer : IDisposable
{
    private readonly Timer _timer;
    private TimeSpan _dueTime;
    private TimeSpan _period;
    private DateTime _nextTick;
    private bool _resuming;
    public SleepAwareTimer(TimerCallback callback, object state, TimeSpan dueTime, TimeSpan period)
    {
        _dueTime = dueTime;
        _period = period;
        _nextTick = DateTime.UtcNow + dueTime;
        SystemEvents.PowerModeChanged += _OnPowerModeChanged;
        _timer = new System.Threading.Timer(o =>
        {
            _nextTick = DateTime.UtcNow + _period;
            if (_resuming)
            {
                _timer.Change(_period, _period);
                _resuming = false;
            }
            callback(o);
        }, state, dueTime, period);
    }
    private void _OnPowerModeChanged(object sender, PowerModeChangedEventArgs e)
    {
        if (e.Mode == PowerModes.Resume)
        {
            TimeSpan dueTime = _nextTick - DateTime.UtcNow;
            if (dueTime < TimeSpan.Zero)
            {
                dueTime = TimeSpan.Zero;
            }
            _timer.Change(dueTime, _period);
            _resuming = true;
        }
    }
    public void Change(TimeSpan dueTime, TimeSpan period)
    {
        _dueTime = dueTime;
        _period = period;
        _nextTick = DateTime.UtcNow + _dueTime;
        _resuming = false;
        _timer.Change(dueTime, period);
    }
    public void Dispose()
    {
        SystemEvents.PowerModeChanged -= _OnPowerModeChanged;
        _timer.Dispose();
    }
}

System.Threading.Timer的公共接口,以及上面从该类复制的子集接口,与您在System.Timers.Timer上看到的不同,但它们完成的是相同的事情。如果您真的想要一个与System.Timers.Timer完全一样的类,那么调整上述技术以满足您的需求应该不难。

系统定时器。计时器是一个基于服务器的计时器(经过的事件使用线程池)。比其他计时器更准确)。当您的计算机进入睡眠模式或休眠模式时,程序的所有状态都存储在RAM中。应用程序状态也是如此。一旦系统启动,操作系统将恢复应用程序状态(以及计时器)。"做点什么"或尝试检测这些事件不是一个好主意。但这可能来自Windows服务。让操作系统来完成它的工作。

一个简单的闹钟类,接受一个绝对的Utc时间,它将在睡眠周期中存活。然后,警报可以从警报回调调整到再次响起。为简单起见,不支持周期模式。基于@PeterDuniho的回答。完全披露:最低限度测试。

using Microsoft.Win32;
using System;
using System.Threading;
class AlarmSleepTolerant : IDisposable
{
    readonly Timer _timer;
    DateTime? _alarmUtcOpt;
    /// <summary>
    /// Constructor
    /// </summary>
    /// <param name="callback"></param>
    /// <param name="alarmUtcOpt"></param>
    /// <param name="stateOpt"></param>
    public AlarmSleepTolerant(TimerCallback callback, DateTime? alarmUtcOpt = null, object stateOpt = null) 
    {
        SystemEvents.PowerModeChanged += _OnPowerModeChanged;
        _timer = new Timer(callback, stateOpt, Timeout.Infinite, Timeout.Infinite);
        SetAlarmTime(alarmUtcOpt);
    }
    
    /// <summary>
    /// Set the current alarm, if alarmUtc is <= UtcNow, then the alarm goes off immediately.
    /// Pass null to disable the alarm.
    /// </summary>
    /// <param name="alarmUtc"></param>
    public void SetAlarmTime(DateTime? alarmUtcOpt = null)
    {
        lock (_timer)
        {
            _alarmUtcOpt = alarmUtcOpt;
            if (!alarmUtcOpt.HasValue)
            {
                _timer.Change(Timeout.Infinite, Timeout.Infinite); // disables the timer
            }
            else
            {
                TimeSpan dueIn = _alarmUtcOpt.Value - DateTime.UtcNow;
                _timer.Change(dueIn.Ticks <= 0 ? 0 : (long)dueIn.TotalMilliseconds, Timeout.Infinite);
            }
        }
    }
    public void Dispose()
    {
        SystemEvents.PowerModeChanged -= _OnPowerModeChanged;
        _timer.Dispose();
    }
    void _OnPowerModeChanged(object sender, PowerModeChangedEventArgs e)
    {
        // Timers are based on intervals rather than absolute times so they 
        // need to be adjusted upon a resume from sleep.
        // 
        // If the alarm callback was missed during sleep, it will be called now.
        //
        if (e.Mode == PowerModes.Resume)
        {
            lock (_timer)
                SetAlarmTime(_alarmUtcOpt);
        }
    }    
}