Как System.Timers.Timer ведет себя в приложении WPF после гибернации и сна?

Я использую System.Timers.Timer в своем приложении WPF. Я хочу понять, как ведет себя Таймер после перехода компьютера в режим гибернации и сна. У меня возникают странные проблемы с моим приложением после того, как компьютер выходит из спящего режима.

Как мне обращаться с таймерами и как они ведут себя, когда компьютер находится в спящем/гибернационном режиме?

У меня есть полуночный таймер, который должен работать каждую полночь, чтобы сбросить значения по умолчанию в пользовательском интерфейсе.

Вот код, который создает таймер:

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();
        }

В конструкторе страницы пользовательского интерфейса я вызываю метод, который вызывает ResestMidnightTimer() и фактически создает таймер. После этого таймер просто ждет ночи.

Когда наступает ночное время (на самом деле это 00:01), таймер срабатывает, сбрасывает значения по умолчанию, как и ожидалось, а затем удаляет существующий таймер. Наконец, он создает новый полуночный таймер на следующий день. Но если я попытаюсь перевести компьютер в спящий режим в этот день, полуночный таймер не сработает и не сбросит значения по умолчанию.

Это потому, что во время спящего режима обработка события просто откладывается на то же время, что и спящий режим?


person User1234    schedule 22.11.2011    source источник


Ответы (4)


Это зависит от того, как вы используете свои таймеры. Если вы используете их для инициирования какого-либо события, которое происходит нечасто (более пары минут), то вы, вероятно, увидите какое-то «странное» поведение. Поскольку вы не указываете, что это за «странное» поведение, я предполагаю, что таймер вашей программы срабатывает позже, чем должен.

Объяснение. Проблема с переходом в спящий режим/спящий режим заключается в том, что все программы приостанавливаются. Это означает, что ваши таймеры не обновляются, и, таким образом, когда вы спите/впадаете в спящий режим и возвращаетесь обратно, это как если бы вы были заморожены на тот период времени, когда вы спали/впали в спящий режим. Это означает, что если у вас есть таймер, настроенный на отключение через час, и ваш компьютер переходит в спящий режим на отметке 15 минут, после того, как он проснется, у него будет еще 45 минут, независимо от того, как долго компьютер находился в спящем режиме.

Решение. Одно из исправлений состоит в том, чтобы сохранить дату и время, когда событие произошло в последний раз. Затем периодически отключайте таймер (каждые 10 секунд или 10 минут, в зависимости от желаемой точности) и проверяйте дату и время последнего выполнения. Если разница между текущим и последним временем выполнения больше или равна желаемому интервалу, ТОГДА вы запускаете выполнение.

Это исправит это так, что если событие «должно» произойти во время сна/гибернации, оно начнется в тот момент, когда вы вернетесь из сна/гибернации.

Обновление: решение, представленное выше, будет работать, и я добавлю пару деталей, чтобы помочь вам реализовать его.

  • Вместо создания/удаления новых таймеров создайте ОДИН таймер для использования, который является ПОВТОРЯЮЩИМСЯ (для свойства AutoReset установлено значение true).

  • Интервал одиночного таймера НЕ должен быть установлен в соответствии со следующим моментом, когда событие должно произойти. Вместо этого для него следует установить выбранное вами значение, которое будет представлять частоту опроса (как часто он проверяет, должно ли выполняться «событие»). Выбор должен быть балансом эффективности и точности. Если вам НУЖНО, чтобы он работал ДЕЙСТВИТЕЛЬНО близко к 00:01, вы устанавливаете интервал около 5-10 секунд. Если менее важно, чтобы это было ровно в 00:01, вы можете увеличить интервал до 1-10 минут.

  • Вам нужно сохранить DateTime, когда произошло последнее выполнение ИЛИ, когда должно произойти следующее выполнение. Я бы предпочел «когда должно произойти следующее выполнение», чтобы вы не выполняли (LastExecutionTime + EventInterval) каждый раз, когда истекает таймер, вы просто будете сравнивать текущее время и время, когда должно произойти событие.

  • По истечении таймера и ДОЛЖНО произойти событие (где-то около 00:01), вы должны обновить сохраненную дату и время, а затем запустить код, который вы хотите запустить в 00:01.

Пояснение по сравнению со спящим и гибернационным режимами. Основное различие между спящим и гибернационным режимом заключается в том, что в спящем режиме все хранится в ОЗУ, а в спящем режиме текущее состояние сохраняется на диск. Основное преимущество режима гибернации заключается в том, что оперативная память больше не нуждается в питании и, следовательно, расходует меньше энергии. Вот почему рекомендуется использовать спящий режим вместо сна при работе с ноутбуками или другими устройствами, использующими ограниченное количество энергии.

Тем не менее, нет никакой разницы в выполнении программ, поскольку они приостанавливаются в любом случае. К сожалению, System.Timers.Timer не «пробуждает» компьютер, поэтому вы не можете принудительно запустить свой код в ~00:01.

Я полагаю, что есть ДРУГИЕ способы «разбудить» компьютер, но если вы не пойдете по этому пути, лучшее, что вы можете сделать, это запустить свое «событие» во время следующего «события опроса» вашего таймера после того, как он выйдет из сна / гибернации.

person docmanhattan    schedule 22.11.2011

Это потому, что во время спящего режима он просто откладывает обработку события на то же время, что и спящий режим?

Пока компьютер находится в приостановленном режиме (то есть в спящем или спящем режиме), он ничего не делает. Это включает в себя, в частности, планировщик, который обрабатывает пробуждение потока, который отслеживает очередь событий таймера, не выполняется, и поэтому этот поток не продвигается к возобновлению выполнения для обработки следующего таймера.

Дело не столько в том, что событие явным образом отложено само по себе. Но да, это чистый эффект.

В некоторых случаях можно использовать класс таймера, у которого нет этой проблемы. И System.Windows.Forms.Timer, и System.Windows.Threading.DispatcherTimer основаны не на планировщике потоков Windows, а на сообщении WM_TIMER. Из-за того, как работает это сообщение, оно генерируется «на лету», когда цикл обработки сообщений потока проверяет очередь сообщений, основываясь на том, прошло ли время истечения срока действия таймера, это похоже на обходной путь опроса, описанный в другой ответ на ваш вопрос невосприимчив к задержкам, которые в противном случае были бы вызваны приостановкой работы компьютера.

Вы заявили, что ваш сценарий включает в себя программу WPF, поэтому вы можете обнаружить, что лучшим решением будет использование класса DispatcherTimer вместо System.Timers.Timer.

Если вы решите, что вам нужна реализация таймера, которая не привязана к потоку пользовательского интерфейса, вот версия 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, вам не составит труда адаптировать описанную выше технику в соответствии с вашими потребностями.

person Peter Duniho    schedule 09.04.2017

System.Timers.Timer — это серверный таймер (прошедшее событие использует Threadpool. Более точный, чем другие таймеры). Когда ваш компьютер переходит в спящий режим или режим гибернации, все состояние вашей программы сохраняется в оперативной памяти. То же самое касается состояния вашего приложения. Как только ваша система включится, состояние вашего приложения будет восстановлено (вместе с таймером) операционной системой. Не будет хорошей идеей «сделать что-то» или попытаться обнаружить эти события. Однако это возможно из службы Windows. Оставьте ОС делать свою работу.

person PRR    schedule 22.11.2011

Простой класс будильника, принимающий абсолютное время 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);
        }
    }    
}
person crokusek    schedule 22.11.2020