Спящий режим, гибернация, Modern Standby и долго работающие приложения — как проектированием предотвратить «остановилось ночью»

· · Спящий режим, Modern Standby, Управление питанием, SetThreadExecutionState, Длительная работа, Таймер, Резидентное приложение, C#, .NET, Расследование сбоев, Разработка Windows, Техническая консультация

«Приложение мониторинга должно было работать всю ночь, но утром график просто обрывается на часу ночи». «Инструмент сбора данных месяцами стабильно работал на настольном компьютере, но стоило заменить его на ноутбук - в записях начали появляться пробелы». Из всех симптомов, встречающихся в консультациях по долго работающим бизнес-приложениям, это классика из классики. Смотришь в лог - ни исключения, ни падения, просто целиком пропали записи за несколько часов. Виновник в подавляющем большинстве случаев - не баг приложения, а управление питанием Windows.

Сложность в том, что слово «сон» - это не одна-единственная вещь. Традиционный спящий режим S3, гибернация (S4) и распространённый в современных ноутбуках Modern Standby (S0 low power idle) выглядят с точки зрения приложения по-разному - и в том, как они его останавливают, и в том, какие меры противодействия работают. Особенно часто неправильно понимают Modern Standby из-за формулировки «система продолжает работать даже во время сна», но на деле десктопные приложения останавливаются ещё активнее, чем в старых моделях.

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

1. Сначала вывод

  • В Windows есть традиционный спящий режим S3, гибернация (S4) и Modern Standby (S0 low power idle), причём машина с поддержкой Modern Standby не поддерживает S1-S3. Узнать, какой режим используется на вашем компьютере, можно через powercfg /a.123
  • Десктопные приложения не продолжают работать даже во время Modern Standby. Desktop Activity Moderator (DAM) приостанавливает потоки десктопных процессов (службы в сессии 0 вместо этого дросселируются). Не проектируйте на основе предположения «раз это S0, значит должно работать».4
  • Во время сна потоки не выполняются, и то, как считается срок таймера, тоже различается по поколениям API. Начиная с Windows 8, относительные таймеры и ожидания (относительная форма SetWaitableTimer, SleepEx и подобные) не считают время сна и переносят оставшееся время на период после возобновления работы.56 Таймеры .NET вплоть до .NET 10 реализованы так, что считают время сна (если срок наступил во время сна, срабатывают сразу после возобновления), а начиная с .NET 11 переходят на схему без учёта сна.6 Среди API истёкшего времени тоже есть те, что включают время сна (GetTickCount, QueryPerformanceCounter - основа Stopwatch), и те, что не включают (QueryUnbiasedInterruptTime).78
  • TCP-соединение, ставшее простаивающим во время сна, может оказаться молча сброшенным по тайм-ауту простоя промежуточного устройства - NAT, файрвола, балансировщика нагрузки (например, Azure Load Balancer по умолчанию сбрасывает его через 4 минуты без уведомления). Проектируйте с расчётом на переподключение после возобновления работы.94
  • Правильный способ подавить сон - SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED). Однако действует это только на автоматический сон из-за тайм-аута бездействия - предотвратить сон, вызванный пользователем через кнопку питания или закрытие крышки, нельзя. Устанавливайте флаг только на нужный участок и обязательно очищайте по завершении.1011
  • Проверить, действует ли подавление, можно через powercfg /requests. У семейства API PowerCreateRequest можно приложить к запросу строку с причиной, которая отображается в этом списке, что заметно упрощает расследование на месте.121314
  • Если вы проектируете с расчётом на сон, обнаруживайте его в .NET через SystemEvents.PowerModeChanged, а в Win32 - через WM_POWERBROADCAST (PBT_APMSUSPEND / PBT_APMRESUMEAUTOMATIC). Отсрочка для уведомления о приостановке составляет всего около 2 секунд на приложение, а при критическом заряде батареи система может уйти в сон вообще без уведомления.15161718
  • Для надёжного запуска в заданное время первый выбор - функция Планировщика заданий «Выводить компьютер из спящего режима для выполнения задачи» (WakeToRun). Она держит систему в бодрствующем состоянии до завершения задачи. Однако она зависит от разрешения таймеров пробуждения в параметрах электропитания, поэтому обязательна проверка пробуждения на реальном оборудовании.1920

2. Сон в Windows - это не одна вещь

Сначала разберём только три состояния, которые нужны как основа для последующих решений.

Состояние Общепринятое название Суть С точки зрения приложения
S3 Традиционный спящий режим CPU остановлен, питание сохраняется только для RAM Никакие вычисления не выполняются1
S4 Гибернация Содержимое памяти записывается в файл гибернации, затем питание отключается То же самое; возобновление - восстановление из этого файла1
S0 low power idle Modern Standby Система остаётся частично работающей при низком энергопотреблении и мгновенно возобновляет работу Десктопные приложения останавливаются DAM24

S3 и S4 - оба «состояния, в которых система не выполняет никаких вычислений и по всем признакам выглядит выключенной».1 Modern Standby, напротив, - модель, близкая к модели питания смартфона: система продолжает ожидать при низком энергопотреблении, сохраняя сетевое соединение даже при выключенном экране, и возобновляет работу менее чем за секунду по нажатию кнопки питания. Машины с поддержкой Modern Standby не используют S1-S3.221

Здесь важен Desktop Activity Moderator (DAM), установленный на машинах с поддержкой Modern Standby. DAM - это механизм, который при переходе в режим ожидания принудительно ограничивает выполнение десктопных приложений до уровня, эквивалентного сну S3: у процессов в интерактивной сессии приостанавливаются все потоки, а службы в сессии 0 дросселируются (приостановлены большую часть времени, работают лишь эпизодически).4 Иными словами, ожидание «раз это Modern Standby, приложение должно продолжать работать во время сна» ровно противоположно реальности: с точки зрения приложения оно останавливается так же, как при S3, и, в отличие от S3, поскольку «сама система работает, а останавливается только приложение», это безопаснее понимать как явление, проявляющееся в виде рассинхронизации времени и несогласованного поведения таймеров.4

Узнать, какая модель используется на вашем (или на компьютере клиента на объекте), можно без прав администратора следующей командой.3

> powercfg /a
Следующие спящие режимы доступны в этой системе:
    Ожидание (S0, низкое энергопотребление в режиме бездействия) - подключение к сети
    Гибернация
    ...

Если выводится Ожидание (S3), машина работает по S3, а если S0, низкое энергопотребление в режиме бездействия - это машина с Modern Standby. Когда речь идёт о жалобе «после замены на ноутбук в записях появились пробелы», это первое, что стоит проверить.

3. Что происходит с приложением во время сна и после возобновления

3.1. Потоки и таймеры

Во время сна (S3/S4 и во время приостановки DAM) потоки не выполняются.14 Легко упустить из виду то, как срок таймера или ожидания учитывает время сна, а это различается в зависимости от слоя API.

  • Относительный таймер Win32 (относительная форма SetWaitableTimer / SetWaitableTimerEx) на Windows 7 и более ранних версиях учитывал время, проведённое в состоянии низкого энергопотребления (обратный отсчёт продолжался даже во время сна), но начиная с Windows 8 не учитывает. Относительный таймер, пересекающий сон, срабатывает только после того, как оставшееся время истечёт уже после возобновления.5
  • Аналогично, API ожидания с тайм-аутом (SleepEx, WaitForMultipleObjectsEx и подобные) начиная с Windows 8 не считают время, не потраченное на выполнение, включая сон. Оставшееся время переносится через сон.6
  • Момент срабатывания управляемого таймера .NET (System.Threading.Timer и подобных) зависит от версии runtime. Это связано с тем, что Environment.TickCount64 вплоть до .NET 10 включает время сна (основан на GetTickCount64), а начиная с .NET 11 не включает (переходит на основу QueryUnbiasedInterruptTime); документация к этому изменению сама предупреждает, что «может существовать код, который перестанет срабатывать сразу после возобновления работы».6

Иначе говоря, если «приложение, измеряющее интервалы через System.Threading.Timer с периодом 10 секунд, проспало 8 часов», оно в любом случае не сработает разом за все 8 часов - но сработает ли оно сразу же после возобновления или подождёт оставшееся время, зависит от слоя API и версии runtime. В любом случае образцы, приходящиеся на время сна, будут потеряны. Безопаснее не полагаться на «срабатывание сразу после возобновления», а явно перепланировать через событие возобновления, описанное в разделе 5.

3.2. Измерение прошедшего времени сбивается

Среди API истёкшего времени есть и включающие, и не включающие время сна.

API Время сна
GetTickCount / GetTickCount64 Включает7
QueryPerformanceCounter (основа Stopwatch в .NET) Включает (standby, hibernate, connected standby)8
QueryUnbiasedInterruptTime Не включает (только время в рабочем состоянии)227
Environment.TickCount / TickCount64 Включает вплоть до .NET 10 (на основе GetTickCount64), меняется на не включает начиная с .NET 11 (на основе QueryUnbiasedInterruptTime)6

Код вида «взять следующий образец, когда Stopwatch покажет 10 секунд», пересекая сон, на деле означает «прошло 8 часов и 10 секунд», а проверка истёкшего времени на основе TickCount, наоборот, меняет поведение при переходе на .NET 11. Чёткое разделение обязанностей - использовать Stopwatch для «сколько заняла операция» и DateTime/DateTimeOffset для «в какое время по настенным часам должно сработать следующее действие», заново привязывая точку отсчёта по событию возобновления (раздел 5) - убережёт вас и от сна, и от обновления runtime. Само по себе проектирование короткопериодных таймеров разобрано в статье «Почему на Windows стоит предпочитать ожидание событий вызову Sleep(1)».

3.3. TCP-соединение умирает «молча»

Во время сна приложение не может обмениваться данными, поэтому соединение переходит в простой. Проблема - в устройствах на пути. Промежуточные устройства вроде NAT, файрволов и балансировщиков нагрузки сбрасывают простаивающий поток по тайм-ауту, но во многих конфигурациях делают это молча, не уведомляя ни одну из сторон. Например, поведение Azure Load Balancer по умолчанию - «молча сбросить поток по достижении тайм-аута простоя (по умолчанию 4 минуты)».9 Похожие тайм-ауты есть и во внутренних офисных маршрутизаторах, и во встроенных стеках TCP на оборудовании на местах.

В результате сокет приложения после возобновления работы выглядит совершенно нормальным, а следующая отправка либо сразу завершается ошибкой, либо зависает в ожидании ответа до тайм-аута. Документация DAM тоже прямо указывает на необходимость учитывать влияние приостановки процесса на время жизни соединения и на рукопожатие в процессе установления связи.4 Более того, на машинах с Modern Standby при работе от батареи сетевая активность во время сна по умолчанию полностью замирает (Adaptive Connected Standby).23 Стандартная практика - считать соединение подозрительным и сбрасывать/переподключать его сразу по получении события возобновления. Для разбора проблемы соединения, которое «выглядит живым, но на деле мертво», см. также статью «Причина остановки связи с промышленной камерой из-за повторной передачи TCP и её диагностика».

4. Как «не давать уснуть» - SetThreadExecutionState и запросы питания

Правильный инструмент для случая, когда сон недопустим только на несколько часов измерения, - SetThreadExecutionState. Из C# его вызывают через P/Invoke.

using System.Runtime.InteropServices;

internal static class PowerGuard
{
    [Flags]
    private enum EXECUTION_STATE : uint
    {
        ES_CONTINUOUS       = 0x80000000,
        ES_SYSTEM_REQUIRED  = 0x00000001,
        ES_DISPLAY_REQUIRED = 0x00000002,
    }

    [DllImport("kernel32.dll", SetLastError = true)]
    private static extern EXECUTION_STATE SetThreadExecutionState(EXECUTION_STATE esFlags);

    /// <summary>Вызывается при начале измерения: подавляет автоматический сон (вызывать из того же потока, что и End)</summary>
    public static void Begin() =>
        SetThreadExecutionState(EXECUTION_STATE.ES_CONTINUOUS |
                                EXECUTION_STATE.ES_SYSTEM_REQUIRED);

    /// <summary>Обязательно вызывается по завершении измерения: снимает подавление (вызывать из того же потока, что и Begin)</summary>
    public static void End() =>
        SetThreadExecutionState(EXECUTION_STATE.ES_CONTINUOUS);
}

Вот особенности, которые нужно учитывать.

  • С флагом ES_CONTINUOUS эффект сохраняется до следующего вызова с тем же флагом. Без него вызов лишь однократно сбрасывает таймер бездействия, поэтому для сохранения эффекта нужно периодически вызывать функцию заново.10
  • ES_SYSTEM_REQUIRED подавляет сон системы, ES_DISPLAY_REQUIRED - выключение дисплея. Для фонового измерения достаточно только первого. Устанавливать ещё и ES_DISPLAY_REQUIRED, когда экран может гаснуть, - это просто трата энергии.10
  • Предотвратить сон, вызванный нажатием кнопки питания или закрытием крышки пользователем, нельзя. Эта функция действует только на автоматический сон из-за тайм-аута бездействия. Официальная документация прямо указывает, что явное действие пользователя должно уважаться.10
  • Система ведёт счёт потоков, вызвавших SetThreadExecutionState, и уходит в сон, когда счётчик обнуляется и нет пользовательского ввода.11 Если процесс аварийно завершается, подавление исчезает вместе с ним, поэтому беспокоиться о «компьютере, который никогда не уснёт из-за забытого снятия подавления», не нужно - в худшем случае это решается перезагрузкой, - но, с другой стороны, это же означает, что это никак не обеспечивает устойчивость к сбою: если процесс упадёт, подавление сна, которое он предоставлял, исчезнет вместе с ним, не оставив никакой защиты для всё ещё выполняющегося измерения.
  • Как следует из названия, эта функция устанавливает состояние выполнения именно вызывающего потока.10 Устанавливайте и снимайте флаг из одного и того же потока. Поскольку продолжение async/await может выполняться на другом потоке из пула потоков, реализация, вызывающая Begin() и End() через await, может привести к тому, что вызов снятия попадёт мимо, оказавшись на другом потоке, а подавление останется установленным, пока жив исходный поток. Безопаснее закреплять эти вызовы за потоком с гарантированной идентичностью, например UI-потоком; если по архитектуре нужно пересекать потоки, используйте API запросов питания на основе хендлов, о котором рассказано ниже.

Начиная с Windows 7 для той же цели доступен и более новый API - запросы питания (PowerCreateRequest / PowerSetRequest / PowerClearRequest). Их практическое преимущество в том, что при создании запроса через REASON_CONTEXT можно передать строку с причиной; помимо PowerRequestSystemRequired, среди типов запроса есть PowerRequestExecutionRequired, подавляющий приостановку процесса на машине с Modern Standby.1413 Официальная лучшая практика - «установить непосредственно перед сценарием, снять сразу после его завершения, освободить хендл перед завершением процесса».13

Однако у машин с Modern Standby есть важное ограничение: на системе с Modern Standby, работающей от батареи, запросы SystemRequired/ExecutionRequired обрываются через 5 минут после превышения тайм-аута сна. Кроме того, независимо от источника питания, при переходе в сон по действию пользователя (кнопка питания, закрытие крышки, сон из меню «Пуск») запрос завершается.13 То есть «продолжать работать на ноутбуке даже с закрытой крышкой, даже от батареи» усилиями одного приложения реализовать нельзя. Такое требование обеспечивается настройками электропитания и эксплуатационной политикой - настройками, не позволяющими уснуть при закрытии крышки, или питанием от сети переменного тока.

Действительно ли подавление работает, можно проверить из командной строки с правами администратора.

> powercfg /requests
SYSTEM:
[PROCESS] \Device\HarddiskVolume3\Apps\SensorLogger.exe

powercfg /requests - команда, перечисляющая запросы питания, которые в данный момент мешают сну или выключению экрана; она полезна и для расследования «компьютера, который почему-то не засыпает», и для проверки подавления собственным приложением.12 Учитывайте также, что администратор может через powercfg /requestsoverride настроить игнорирование запросов конкретного процесса.12 Базовая предпосылка проектирования в том, что API подавления - это «просьба», а не абсолютная гарантия.

Напоследок - о хорошем тоне. Реализация, оставляющая ES_CONTINUOUS | ES_SYSTEM_REQUIRED установленным всё время работы приложения, означает, что резидентное приложение постоянно перекрывает план электропитания, настроенный пользователем. На ноутбуке это разряжает батарею, а на общем компьютере влияет и на другие сценарии использования. Принцип - ограничивать подавление только «участком, где реально выполняется операция, не допускающая сна» (пример в самой официальной документации - «установить при начале записи, снять по завершении записи»10). Кстати, в качестве временного обходного пути для пользователя есть PowerToys Awake, которая внутри тоже работает по тому же механизму - поток, запрашивающий состояние выполнения.24 Это же можно использовать как ориентир для того, где провести границу между реализацией подавления в приложении и передачей этого на откуп эксплуатационному инструменту.

5. Как «рассчитывать на сон» - обнаружение, переподключение, запись пробелов

Для большинства резидентных приложений мониторинга или сбора данных сосуществование со сном оказывается более разумным решением, чем полный запрет сна. Нужны три вещи: узнать заранее, узнать о возобновлении, восстановить состояние после возобновления.

В .NET точкой входа служит Microsoft.Win32.SystemEvents.PowerModeChanged.15

using Microsoft.Win32;

SystemEvents.PowerModeChanged += OnPowerModeChanged;

private void OnPowerModeChanged(object sender, PowerModeChangedEventArgs e)
{
    switch (e.Mode)
    {
        case PowerModes.Suspend:
            // Отсрочка короткая: ограничиваемся сбросом буферов и записью времени остановки измерения
            _logger.Info("suspend at {0:O}", DateTimeOffset.Now);
            _collector.Pause();
            break;

        case PowerModes.Resume:
            // 1) Записываем сам пробел как данные
            _logger.Info("resume at {0:O}", DateTimeOffset.Now);
            // 2) Считаем соединение мёртвым, отбрасываем и переподключаемся
            _connection.Reset();
            // 3) Пересчитываем расписание относительно настенных часов и заново взводим таймеры
            _scheduler.Rebase(DateTimeOffset.Now);
            // 4) Явно возобновляем сбор только после завершения восстановления (не оставляем на паузе)
            _collector.Resume();
            break;
    }
}

У этого события официально отмечены два нюанса: оно не срабатывает, если не работает насос сообщений (в службе Windows для этого нужна, например, скрытая форма), и поскольку это статическое событие, отсутствие отписки приводит к утечке.15 В GUI-приложении его можно использовать напрямую, но в консольном или сервисном приложении сбора данных нужно либо самостоятельно завести окно сообщений для приёма WM_POWERBROADCAST из Win32, либо использовать PowerRegisterSuspendResumeNotification, позволяющий получать обратный вызов вообще без HWND (в среде DAM уведомления идут по этому же пути).416

Разберём также смысл событий на уровне Win32.

  • PBT_APMSUSPEND: уведомление непосредственно перед сном. Отсрочка составляет всего около 2 секунд на приложение, и её превышение может привести к прерыванию обработки системой. Здесь стоит ограничиться сбросом буферов и записью времени, а не писать что-то длительное вроде сетевой очистки.1718
  • PBT_APMRESUMEAUTOMATIC: уведомление, гарантированно приходящее при каждом возобновлении. Именно здесь пишите переподключение и перепланирование.25
  • PBT_APMRESUMESUSPEND: приходит после PBT_APMRESUMEAUTOMATIC, когда возобновление вызвано действием пользователя (или пользователь вернулся). Не приходит при автоматическом возобновлении вроде удалённого пробуждения, поэтому если «обработку возобновления» написать только здесь, при безлюдном возобновлении восстановление не запустится.26
  • Кроме того, при критическом переходе в сон (например, из-за критического заряда батареи) предварительное уведомление вообще не приходит.18 Пишите обработку на стороне возобновления идемпотентной, чтобы она работала корректно даже в случае, когда уведомление о приостановке не было получено вовсе.

Ещё одна вещь, не менее важная, чем сама обработка возобновления, - записывать пробел именно как пробел. Данные сбора, пересекающие сон, - это не «отсутствующее значение», а «не измерено, потому что система была остановлена», и если хранить этот интервал вместе с отметками времени suspend/resume и в логе, и в данных, тот, кто впоследствии увидит пустоту на графике, не спутает её со сбоем. О том, что должны сохранять логи долго работающего приложения, также рассказано в статье «Расследование сбоя промышленной камеры при длительной работе - случай утечки хендлов».

Отметим также похожую на сон проблему «вроде бы всё стало медленнее» - дросселирование через режим эффективности (EcoQoS) в Windows 11. Об этом рассказано в статье «Что такое режим эффективности Windows - зелёный значок листа и способ отключения».

6. Как надёжно запускать в заданное время - пробуждение через Планировщик заданий

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

Задача с включённым WakeToRun пробуждает компьютер из сна или гибернации в назначенное время и держит систему в бодрствующем состоянии до завершения задачи (если система уже не спала, то же самое требование поддерживается до завершения). При пробуждении экран может оставаться выключенным - это нормально.19

Однако для успешного пробуждения есть условия. Как указано и в официальной документации по устранению неполадок, необходимы включённая настройка «Разрешить таймеры пробуждения» в параметрах электропитания и включённая настройка пробуждения на стороне BIOS, а современные ноутбуки по соображениям энергосбережения нередко сконфигурированы так, что вообще не разрешают пробуждение.20 Через powercfg /waketimers можно перечислить текущие активные таймеры пробуждения,12 так что обязательно проверяйте на реальном развёрнутом оборудовании, действительно ли происходит пробуждение. Если нужно будить машину из собственного приложения, есть также вариант пробуждающего таймера - передача TRUE в fResume у SetWaitableTimer. В этом случае после автоматического пробуждения система остаётся бодрствующей только на время безлюдного таймера бездействия (минимум 2 минуты) и быстро уходит обратно в сон, если приложение не объявит себя «используемым» через SetThreadExecutionState. Если обработка после пробуждения занимает много времени, правильное сочетание - объединить это с подавлением из раздела 4.27

Вопросы эксплуатации самой задачи - учётная запись выполнения, проблема завершения с кодом 0x1, предотвращение многократного запуска - разобраны в статье «Задачи Планировщика заданий не выполняются или завершаются с кодом 0x1 - диагностика причин и безопасное проектирование эксплуатации».

7. Таблица решений - подавление, обработка возобновления или пробуждение

Три подхода к проектированию не исключают друг друга - для каждого типа приложения выбирается основной и вспомогательный, и они комбинируются.

Тип приложения Первый выбор Комбинация / примечания
Измерение / сбор данных (непрерывное измерение от часов до дней) Подавление через ES_SYSTEM_REQUIRED, ограниченное окном измерения Обязательно реализовать и обработку возобновления (сон по действию пользователя предотвратить нельзя10). На машинах с Modern Standby при питании от батареи действует отсечка через 5 минут, поэтому требуйте питание от сети13
Ночное пакетное задание / плановая передача Планировщик заданий + WakeToRun19 Более энергоэффективно и надёжно, чем резидентный процесс с подавлением. Обязательна проверка разрешения таймера пробуждения20
Резидентный мониторинг / агент уведомлений Расчёт на сон (обнаружение через PowerModeChanged → переподключение, перепланирование, запись пробелов)15 Для выделенной машины, чья единственная цель - мониторинг, отключайте сон на уровне плана электропитания, а не в приложении
Инструмент отображения на десктопе (презентации, дашборды и т. п.) Использовать ES_DISPLAY_REQUIRED вместе с ES_SYSTEM_REQUIRED, ограничив временем отображения10 Обязательно снимать по завершении отображения. Для устройства с постоянным отображением решать через настройки электропитания
Управление оборудованием 24/7, компьютер линии Полностью отключить сон через настройки электропитания (обеспечивается эксплуатационно) Не полагаться на API подавления в приложении как на страховку. Использовать powercfg /requests для периодических проверок12

У решения два измерения: «есть ли окно времени, когда остановка допустима» (если да - Планировщик заданий, если нет - настройки электропитания) и «на чьём компьютере это работает» (чем больше приложение работает на личном или общем ноутбуке пользователя, тем больше стоит склоняться к обработке возобновления, а не к подавлению).

8. Итог

  • Сон бывает S3, гибернация (S4) и Modern Standby (S0 low power idle), различить их можно через powercfg /a. Даже на машине с Modern Standby десктопные приложения приостанавливаются DAM, поэтому предположение «продолжает работать даже во время сна» не работает.
  • Во время сна не продвигаются ни потоки, ни таймеры. Начиная с Windows 8 относительные таймеры и ожидания не считают время сна и переносят остаток, а таймеры .NET получают такое же поведение начиная с .NET 11 (вплоть до .NET 10 они могут срабатывать сразу после возобновления). Среди API истёкшего времени есть и включающие, и не включающие время сна. Ведите истёкшее время через Stopwatch, а настенное время - через DateTime, заново привязывая точку отсчёта при возобновлении.
  • TCP-соединение молча умирает по тайм-ауту простоя промежуточного устройства. Стандартная практика - сбрасывать и переподключаться по событию возобновления.
  • Подавляйте через SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED) только на нужном участке. Сон по действию пользователя предотвратить нельзя, а на машине с Modern Standby при питании от батареи подавление обрывается через 5 минут. Проверяйте через powercfg /requests.
  • Обрабатывайте возобновление через SystemEvents.PowerModeChanged / WM_POWERBROADCAST. Поскольку отсрочка уведомления о приостановке составляет всего около 2 секунд, а сон может произойти вообще без уведомления, пишите обработку возобновления идемпотентной и записывайте интервал пробела.
  • Для запуска в заданное время настоящее решение - WakeToRun в Планировщике заданий. Проектируйте с учётом и настройки электропитания для таймера пробуждения, и проверки пробуждения на реальном оборудовании.

Похожие статьи

Смежные области консультаций

В Komura Software LLC мы занимаемся проектированием и реализацией долго работающих Windows-приложений для измерений, мониторинга и сбора данных, а также расследованием сбоев, специфичных для длительной работы, - «остановилось ночью», «в записях появляются пробелы». Обращайтесь к нам и по диагностике трудно воспроизводимых симптомов, связанных со сном и управлением питанием.

Справочные материалы

  1. Microsoft Learn, System Sleeping States. О том, что S1-S4 - это состояния сна, в которых не выполняются вычисления, что S3 сохраняет только память, а S4 сохраняет её в файл гибернации, и о том, что powercfg /a позволяет перечислить доступные в системе спящие режимы.  2 3 4 5

  2. Microsoft Learn, System power states. О том, что в режиме S0 low power idle (Modern Standby) система остаётся частично работающей при низком энергопотреблении, что системы на SoC с поддержкой Modern Standby не используют S1-S3, и о том, что при критическом переходе уведомление не выдаётся.  2 3

  3. Microsoft Learn, Overview of Modern Standby Testing and Diagnostics. О том, что powercfg /a позволяет определить поддержку Modern Standby (по записи Standby (S0 Low Power Idle) в выводе).  2

  4. Microsoft Learn, Desktop Activity Moderator. О том, что DAM принудительно ограничивает выполнение десктопных приложений до уровня, эквивалентного S3, что у процессов в интерактивной сессии приостанавливаются все потоки, а сессия 0 дросселируется, об уведомлении WM_POWERBROADCAST перед приостановкой, о несогласованности поведения таймеров/времени работы с настенными часами, и о необходимости учитывать время жизни соединений.  2 3 4 5 6 7 8

  5. Microsoft Learn, SetWaitableTimerEx function. О том, что таймер с относительным временем на Windows 7 и более ранних версиях включал время, проведённое в состоянии низкого энергопотребления (обратный отсчёт продолжался и во время сна), тогда как начиная с Windows 8 не включает (обратный отсчёт не продвигается во время сна).  2

  6. Microsoft Learn, Environment.TickCount made consistent with Windows timeout behavior. О критическом изменении, при котором Environment.TickCount/TickCount64 вплоть до .NET 10 основан на GetTickCount64 (включает время сна), а начиная с .NET 11 переходит на основу QueryUnbiasedInterruptTime (не включает), о том, что API ожидания с тайм-аутом (SleepEx/WaitForMultipleObjectsEx) уже перестали считать нерабочее время начиная с Windows 8, и о том, что это изменение может привести к тому, что код перестанет срабатывать сразу после возобновления.  2 3 4 5

  7. Microsoft Learn, Windows Time. О том, что истёкшее время из GetTickCount/GetTickCount64 включает время сна и гибернации, а QueryUnbiasedInterruptTime включает только время в рабочем состоянии.  2 3

  8. Microsoft Learn, Acquiring high-resolution time stamps. О том, что System.Diagnostics.Stopwatch в управляемом коде использует QPC как основу времени, и что QueryPerformanceCounter возвращает число тактов, включающее время, проведённое в standby, hibernate и connected standby.  2

  9. Microsoft Learn, Load Balancer TCP Reset and Idle Timeout. О том, что поведение балансировщика нагрузки по умолчанию - молча сбрасывать поток по достижении тайм-аута простоя (по умолчанию 4 минуты), что отправка TCP-сброса - функция, которую нужно включать явно, и что в качестве меры противодействия используется TCP keep-alive.  2

  10. Microsoft Learn, SetThreadExecutionState function. О значении ES_CONTINUOUS/ES_SYSTEM_REQUIRED/ES_DISPLAY_REQUIRED, о том, что вызов без ES_CONTINUOUS лишь однократно сбрасывает таймер бездействия, что сон, вызванный пользователем, предотвратить нельзя, и о примере использования - устанавливать только на время нужной операции и снимать по завершении.  2 3 4 5 6 7 8

  11. Microsoft Learn, System Sleep Criteria. О том, что система считает приложения/потоки, вызвавшие SetThreadExecutionState, и уходит в сон, когда этот счётчик равен нулю и нет пользовательского ввода.  2

  12. Microsoft Learn, Powercfg command-line options. О том, что /requests перечисляет запросы питания, мешающие сну или выключению экрана, что /requestsoverride позволяет игнорировать запросы конкретного процесса, и что /waketimers позволяет перечислить активные таймеры пробуждения.  2 3 4 5

  13. Microsoft Learn, PowerSetRequest function. О типах запроса, таких как PowerRequestSystemRequired/PowerRequestExecutionRequired, о том, что на системе Modern Standby при питании постоянным током запрос обрывается через 5 минут после превышения тайм-аута сна, что запрос завершается при переходе в сон по действию пользователя, и о лучшей практике - прикреплять строку с причиной и вызывать Set непосредственно перед сценарием, а Clear сразу после.  2 3 4 5

  14. Microsoft Learn, PowerCreateRequest function. О создании объекта запроса питания с указанием REASON_CONTEXT и об освобождении через CloseHandle, когда он больше не нужен.  2

  15. Microsoft Learn, SystemEvents.PowerModeChanged Event. О том, что это событие, срабатывающее при приостановке/возобновлении, что оно не срабатывает без работающего насоса сообщений (для службы нужна, например, скрытая форма), и что, будучи статическим событием, оно приводит к утечке при отсутствии отписки.  2 3 4

  16. Microsoft Learn, WM_POWERBROADCAST message. О том, что события управления питанием доставляются окну как сообщение WM_POWERBROADCAST (PBT_APMSUSPEND / PBT_APMRESUMEAUTOMATIC / PBT_APMRESUMESUSPEND и т. п.).  2

  17. Microsoft Learn, PBT_APMSUSPEND event. О том, что это уведомление отправляется непосредственно перед приостановкой, и что отсрочка на обработку составляет около 2 секунд, а её превышение может привести к прерыванию обработки системой.  2

  18. Microsoft Learn, System Power Management Events. О том, что при экстренной приостановке, например из-за критического заряда батареи, предварительное уведомление не выдаётся, и что обработка уведомления о приостановке ограничена тайм-аутом максимум 2 секунды на приложение.  2 3

  19. Microsoft Learn, ITaskSettings::get_WakeToRun method. О том, что WakeToRun пробуждает компьютер для выполнения задачи и держит его бодрствующим до завершения задачи, и что экран при пробуждении иногда остаётся выключенным.  2 3

  20. Microsoft Learn, Automatic maintenance. О том, что в списке проверок при неработающем плановом пробуждении указаны настройка пробуждения BIOS, параметр электропитания «Allow Wake Timer» и настройка WakeToRun задачи, и что для современных ноутбуков обычна конфигурация, не разрешающая пробуждение S3.  2 3

  21. Microsoft Learn, What is Modern Standby. О том, что Modern Standby достигает мгновенного включения/выключения при сохранении сетевого соединения по модели S0 low power idle, и что возобновление (от кнопки питания до включения экрана) занимает менее секунды. 

  22. Microsoft Learn, QueryUnbiasedInterruptTime function. О том, что unbiased interrupt time считает только время в рабочем состоянии и не включает время сна и гибернации. 

  23. Microsoft Learn, Modern standby network connectivity. О том, что Adaptive Connected Standby замораживает сетевую активность во время сна при питании от батареи, если нет сценария, требующего иного. 

  24. Microsoft Learn, PowerToys Awake utility. О том, что это утилита, удерживающая компьютер бодрствующим без изменения плана электропитания, работающая через порождение фонового потока, запрашивающего состояние машины, и возвращающая обычное поведение плана электропитания после завершения. 

  25. Microsoft Learn, PBT_APMRESUMEAUTOMATIC event. О том, что это событие доставляется при каждом возобновлении без исключений, само по себе не свидетельствуя о присутствии пользователя, и что при обнаружении активности пользователя вслед за ним доставляется PBT_APMRESUMESUSPEND. 

  26. Microsoft Learn, PBT_APMRESUMESUSPEND event. О том, что это доставляется после PBT_APMRESUMEAUTOMATIC при возобновлении, вызванном пользователем или при обнаружении пользователя, и что при удалённом пробуждении доставляется только PBT_APMRESUMEAUTOMATIC. 

  27. Microsoft Learn, System Wake-up Events. О том, что установка TRUE в fResume у SetWaitableTimer позволяет таймеру будить систему, и что после автоматического пробуждения устанавливается минимум 2-минутный безлюдный таймер бездействия, по истечении которого система возвращается в сон, если SetThreadExecutionState не указывает на использование. 

Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.

Если ваше Windows-приложение приняли за вирус — как реагировать на ложные срабатывания Microsoft Defender и жить с влиянием на производительность

Разбираем правильный порядок действий, если Microsoft Defender ложно определяет ваше Windows-приложение как вредоносное: как устроена сов...

Значки в области уведомлений и всплывающие (toast) уведомления в Windows-приложениях — подводные камни NotifyIcon и выбор правильного AppNotification

Практическое руководство о том, как удерживать бизнес-приложение Windows в области уведомлений (system tray) и оповещать пользователя с п...

Эти страницы показывают тему статьи в более широком контексте услуг и решений.

Статья напрямую связана со следующими услугами.

Частые вопросы

Вопросы, которые часто возникают при консультациях по теме статьи.

Как не давать компьютеру уходить в сон только пока работает моё приложение?
Базовый подход - вызвать SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED) в начале операции и очистить через SetThreadExecutionState(ES_CONTINUOUS) по её завершении. Флаг ES_DISPLAY_REQUIRED добавляйте, только если нужно ещё и не гасить экран. Это действует лишь на автоматический сон из-за тайм-аута бездействия - предотвратить сон, который пользователь вызвал явно (кнопкой питания или закрытием крышки), нельзя. Убедиться, что подавление реально действует, можно через powercfg /requests.
Что такое Modern Standby и чем это отличается от обычного сна?
Это более новая модель сна, также называемая S0 low power idle, при которой система остаётся частично активной при низком энергопотреблении и может мгновенно возобновить работу. Машина с поддержкой Modern Standby не поддерживает традиционный режим сна S3. Однако «продолжает работать» только та активность, которую явно разрешила ОС - у десктопных приложений Desktop Activity Moderator (DAM) приостанавливает все потоки, поэтому с точки зрения приложения оно останавливается точно так же, как при S3. Узнать, какой режим использует ваш компьютер, можно через powercfg /a.
Почему после выхода из сна перестаёт работать TCP-соединение?
Поскольку во время сна приложение не может обмениваться данными, соединение переходит в состояние простоя, и промежуточные устройства на пути - NAT, файрволы, балансировщики нагрузки - сбрасывают поток по тайм-ауту простоя. Многие устройства делают это молча, не уведомляя ни одну из сторон, поэтому сокет приложения выглядит совершенно нормальным вплоть до первой отправки или получения данных после выхода из сна, которая либо сразу завершается ошибкой, либо зависает до тайм-аута в ожидании ответа. Стандартная практика - считать соединение подозрительным и пересоздавать его сразу при получении события возобновления работы.
Для надёжного запуска ночного пакетного задания лучше подавлять сон или использовать Планировщик заданий?
Первый выбор - функция Планировщика заданий «Выводить компьютер из спящего режима для выполнения задачи» (WakeToRun). Она пробуждает компьютер в назначенное время и держит его в бодрствующем состоянии до завершения задачи, поэтому не нужно убивать сон на всю ночь. Однако пробуждение не сработает, если таймеры пробуждения отключены в параметрах электропитания, поэтому обязательно проверьте, что параметр «Разрешить таймеры пробуждения» включён, и убедитесь на реальном оборудовании, что пробуждение действительно происходит. Постоянное подавление сна всё это время впустую расходует энергию и представляет собой недобросовестное проектирование, перекрывающее собственные настройки электропитания пользователя.

Об авторе

Страница с профилем автора статьи.

Го Комура

Представитель KomuraSoft LLC

Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.

Публичные ссылки

Вернуться в блог