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

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

История изменений (1 обновлений, последнее 30 Aug 2026)

Журнал изменений этой статьи. Там, где версия до правки была заархивирована, она остаётся доступной для чтения по постоянной ссылке с DOI.

Русский текст переписан как полноценный технический перевод, а не калька с японского. Утверждения статьи не менялись.
Первая публикация
Цитирование статьи(DOI (зарегистрированный архив): 10.5281/zenodo.21620056)

Приведённые ниже DOI относятся к ранее зарегистрированным архивным версиям, которые могут отличаться от текущего текста. Для ссылки на текущий текст используйте URL этой страницы.

Го Комура (2026). Спящий режим, гибернация и Modern Standby: как не дать долгоживущему приложению остановиться ночью. KomuraSoft LLC. https://comcomponent.com/ru/blog/windows-sleep-modern-standby-long-running-apps/

DOI (зарегистрированный архив)
10.5281/zenodo.21620056
DOI (последняя зарегистрированная версия)
10.5281/zenodo.21620057

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

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

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

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

Выводов много, поэтому они разложены на три части: исходные факты (1.1), проект, в котором сон не допускают (1.2) и проект, который со сном считается (1.3). Короткий путь — прочитать 1.1, решить, ваше приложение ближе к 1.2 или к 1.3, и уже затем идти в основной текст (таблица решений — в главе 7).

1.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 их переводят на схему без учёта сна (.NET 11 на момент написания статьи ещё не вышел; см. оговорку в 3.1).6 Среди API прошедшего времени одни включают время сна (GetTickCount, QueryPerformanceCounter — основа Stopwatch), другие не включают (QueryUnbiasedInterruptTime).78
  • TCP-соединение, которое во время сна ушло в простой, могут молча сбросить по тайм-ауту простоя NAT, брандмауэр, балансировщик нагрузки и другие промежуточные устройства (у Azure Load Balancer по умолчанию — 4 минуты, без уведомления). После возобновления проектируйте с расчётом на новое соединение.94

1.2. Проект, в котором сон не допускают (глава 4)

  • Прямой способ подавить сон — SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED). Действует он только на автоматический сон по тайм-ауту бездействия: сон по кнопке питания или закрытию крышки этим API не остановить. Ставьте запрос только на нужный интервал и по окончании обязательно снимайте.1011
  • Что подавление реально действует, проверяют командой powercfg /requests. У семейства PowerCreateRequest к запросу можно приложить строку с причиной — она попадает в этот список, и разбирать ситуацию на месте становится проще.121314
  • Если требование такое, как у ПК на оборудовании, который должен работать круглосуточно — «сну вообще не место», — отключать сон нужно в параметрах электропитания, а не API подавления в приложении (конкретные места настроек — в 7.1).

1.3. Проект, который со сном считается (главы 5 и 6)

  • Если сон заложен в проект, в .NET его ловят через SystemEvents.PowerModeChanged, в Win32 — через WM_POWERBROADCAST (PBT_APMSUSPEND / PBT_APMRESUMEAUTOMATIC). На уведомление о приостановке у приложения около 2 секунд; при критически низком заряде батареи система может уснуть вообще без уведомления.15161718
  • Консольные приложения и службы без окна получают те же уведомления через PowerRegisterSuspendResumeNotification, без HWND (минимальный пример — в 5.1).19
  • Чтобы работа гарантированно пошла в заданный момент, первый кандидат — флажок Планировщика заданий «Пробуждать компьютер для выполнения задачи» (WakeToRun). Он держит систему бодрствующей до конца задания. Но это зависит от разрешения таймеров пробуждения в параметрах электропитания, поэтому на целевом железе нужно проверить, что машина действительно просыпается.2021

На схеме сплошная линия обозначает отношение, которое выполняется всегда, а пунктирная — условное отношение (условия указаны в пояснении к каждому отношению на странице сведений). Полный список отношений (всего 19, с доказательствами и степенью уверенности) и определения основных понятий собраны на странице сведений карты знаний (на японском). Данные: JSON-LD / Turtle

2. Сон в Windows — это не одно состояние

Сначала — только три состояния, без которых нельзя выбирать меры.

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

S3 и S4 — это «состояния, в которых система не выполняет вычислительных задач и снаружи выглядит выключенной».1 Modern Standby ближе к модели питания смартфона: при выключенном экране система ждёт при низком энергопотреблении, сохраняя сетевое соединение, и возвращается к работе менее чем за секунду после нажатия кнопки питания. Устройства с Modern Standby не используют S1–S3.222

Важная деталь машин с Modern Standby — Desktop Activity Moderator (DAM). 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. Если выстроить так же, как таблицу API прошедшего времени в следующем разделе, получится следующее.

Тип таймера или ожидания Во время сна После возобновления
Относительный таймер Win32 (относительная форма SetWaitableTimer / SetWaitableTimerEx) Начиная с Windows 8 отсчёт не идёт (до Windows 7 включительно в срок входило и время в состоянии низкого энергопотребления)5 Срабатывает, когда истечёт оставшееся время5
API ожидания с тайм-аутом (SleepEx / WaitForMultipleObjectsEx и аналоги) Начиная с Windows 8 время простоя не считают6 Ожидание продолжается на оставшееся время6
Управляемый таймер .NET (System.Threading.Timer и аналоги) До .NET 10 включительно считают (на базе GetTickCount64), с .NET 11 не будут считать (на базе QueryUnbiasedInterruptTime)6 До .NET 10 включительно срабатывают сразу после возобновления, если срок уже прошёл; с .NET 11 сначала ждут остаток6
Таймер с пробуждением (fResume = TRUE у SetWaitableTimer) Будит саму систему23 Срабатывает после пробуждения, но если за время таймера бездействия без пользователя (минимум 2 минуты) не объявить, что система «занята», она снова уснёт (глава 6)23

Документ к изменению в третьей строке описывает его как ломающее изменение из-за смены основы Environment.TickCount64 и сам предупреждает: «может найтись код, который перестанет срабатывать сразу после возобновления».6

Про формулировки про .NET 11

На момент написания статьи (июль 2026) .NET 11 ещё не вышел. У .NET ежегодный цикл мажорных выпусков в ноябре; ближайший .NET 10 вышел 11 ноября 2025 года.24 Поведение .NET 11 в этой статье — план по опубликованному документу о ломающем изменении (ещё на стадии preview). После GA сверяйтесь с актуальной документацией.6

Если «приложение мерило интервалы System.Threading.Timer по 10 секунд и провело во сне 8 часов», пачкой за все 8 часов оно всё равно не сработает. Но сработает ли один раз сразу после возобновления или сначала дождётся остатка — зависит от слоя API и версии среды. В любом случае отсчёты за время сна будут пропущены. Не опирайтесь на «срабатывание сразу после возобновления» как на способ восстановиться: безопаснее явно пересчитать расписание по событию возобновления из главы 5.

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

Среди API прошедшего времени одни включают время сна, другие — нет.

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

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

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

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

В итоге после возобновления сокет выглядит исправным: следующая отправка либо сразу завершается ошибкой, либо зависает в ожидании ответа до тайм-аута. Документация DAM прямо требует учитывать влияние приостановки процесса на время жизни соединения и на рукопожатие.4 На машинах с Modern Standby при питании от батареи сетевая активность во сне по умолчанию и вовсе останавливается (Adaptive Connected Standby).26 Принятая практика — по событию возобновления считать соединение подозрительным, закрывать его и устанавливать заново. Как отличать соединение, которое «выглядит живым, но уже мертво», см. также «Причина остановки связи с промышленной камерой из-за повторной передачи 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
  • Сон по нажатию кнопки питания или закрытию крышки этим API не остановить. Функция действует только на автоматический сон по тайм-ауту бездействия. В официальной документации прямо сказано, что явное действие пользователя следует уважать.10
  • Система считает потоки, вызвавшие SetThreadExecutionState, и уходит в сон, когда счётчик обнуляется и нет ввода от пользователя.11 Если процесс аварийно завершится, подавление исчезнет вместе с ним, поэтому «ПК, который никогда не уснёт из-за забытого снятия», после перезагрузки приходит в норму. Обратная сторона: это не защита от сбоя.
  • Как видно из имени, функция задаёт состояние выполнения вызывающего потока.10 Ставить и снимать запрос нужно из одного и того же потока. Продолжение async/await может пойти на другом потоке из пула, поэтому Begin() и End() через await рискуют снять запрос не с того потока: снятие промахивается, а исходный поток держит подавление, пока жив. Надёжнее вызывать это с потока с устойчивой идентичностью — например, с UI-потока. Если по архитектуре нужно пересекать потоки, используйте API запросов питания на дескрипторах, о котором ниже.

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

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

Что подавление реально действует, проверяют в командной строке с правами администратора.

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

powercfg /requests перечисляет запросы питания, которые сейчас мешают сну или выключению экрана. Команда полезна и когда «ПК почему-то не засыпает», и когда нужно проверить подавление своим приложением.12 Как читать вывод:

  • Вывод разбит заголовками по типу запроса. Базовые — DISPLAY (не гасить экран), SYSTEM (не уходить в сон), AWAYMODE; для PowerRequestExecutionRequired появляется ещё EXECUTION. Если в примере под SYSTEM: видно ваше приложение, системный сон сейчас подавлен.1213
  • В начале строк под заголовком стоят [PROCESS] / [SERVICE] / [DRIVER] — тип источника. Этот тип и имя можно сразу передать в powercfg /requestsoverride.12
  • Если запросов данного типа нет, выводится строка об отсутствии (в английской среде — None.). Чтобы проверить подавление своим приложением, достаточно смотреть, есть ли ваш процесс под SYSTEM.
  • Если через API семейства PowerCreateRequest приложили строку с причиной, она попадает в этот список: эксплуатация видит, ради какой операции сон держат.1413

Имейте в виду и обратное: администратор может через powercfg /requestsoverride велеть игнорировать запросы конкретного процесса.12 API подавления — это «просьба», а не гарантия. Так и задумано.

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

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

5.1. Как принять возобновление в приложении без окна

У сборщика или службы Windows без окна меньше возни с PowerRegisterSuspendResumeNotification, чем с самодельным окном сообщений. В Flags передают DEVICE_NOTIFY_CALLBACK, в Recipient — указатель на DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS с обратным вызовом и контекстом; при успехе функция возвращает ERROR_SUCCESS (0). Доступно начиная с Windows 8 / Windows Server 2012.1928

using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;

// Минимальный пример: консоль/служба без окна принимает приостановку и возобновление
// Цикл обработки сообщений не нужен. Windows 8 / Windows Server 2012 и новее
internal sealed class SuspendResumeNotifier : IDisposable
{
    private const uint DEVICE_NOTIFY_CALLBACK  = 2;      // powrprof.h
    private const uint PBT_APMSUSPEND          = 0x0004; // winuser.h
    private const uint PBT_APMRESUMEAUTOMATIC  = 0x0012; // winuser.h
    private const uint ERROR_SUCCESS           = 0;

    // ULONG DeviceNotifyCallbackRoutine(PVOID Context, ULONG Type, PVOID Setting)
    private delegate uint DeviceNotifyCallbackRoutine(IntPtr context, uint type, IntPtr setting);

    [StructLayout(LayoutKind.Sequential)]
    private struct DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS
    {
        public IntPtr Callback;
        public IntPtr Context;
    }

    [DllImport("powrprof.dll")]
    private static extern uint PowerRegisterSuspendResumeNotification(
        uint flags,
        ref DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS recipient,
        out IntPtr registrationHandle);

    [DllImport("powrprof.dll")]
    private static extern uint PowerUnregisterSuspendResumeNotification(IntPtr registrationHandle);

    // Делегат держим в поле (локальную переменную GC заберёт, и при уведомлении процесс упадёт)
    private readonly DeviceNotifyCallbackRoutine _callback;
    private IntPtr _registration;

    // Регистрация, которую не удалось снять, остаётся на стороне ОС.
    // Этот список держит только её делегат до конца процесса (см. Dispose ниже)
    private static readonly List<DeviceNotifyCallbackRoutine> AbandonedCallbacks = new();

    public SuspendResumeNotifier()
    {
        _callback = OnPowerNotification;

        var parameters = new DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS
        {
            Callback = Marshal.GetFunctionPointerForDelegate(_callback),
            Context  = IntPtr.Zero,
        };

        uint result = PowerRegisterSuspendResumeNotification(
            DEVICE_NOTIFY_CALLBACK, ref parameters, out _registration);

        if (result != ERROR_SUCCESS)
        {
            throw new InvalidOperationException(
                $"PowerRegisterSuspendResumeNotification failed with {result}.");
        }
    }

    private uint OnPowerNotification(IntPtr context, uint type, IntPtr setting)
    {
        // Этот метод вызывает напрямую ОС (нативная сторона). Если выпустить
        // управляемое исключение за границу, на управляемой стороне некому
        // его поймать — падает весь процесс. Переполненный приёмник логов,
        // уже уничтоженный collector и другие вполне реальные сбои тогда
        // убивают приложение при каждом сне. Обработку замыкаем внутри
        // и всегда возвращаем код
        try
        {
            switch (type)
            {
                case PBT_APMSUSPEND:
                    // Около 2 секунд. Только сброс буферов и запись момента остановки
                    _logger.Info("suspend at {0:O}", DateTimeOffset.Now);
                    _collector.Pause();
                    break;

                case PBT_APMRESUMEAUTOMATIC:
                    // Приходит и при возобновлении без пользователя. Тяжёлое восстановление — вне обратного вызова
                    _logger.Info("resume at {0:O}", DateTimeOffset.Now);
                    _recovery.RequestRebuild();
                    break;
            }
        }
        catch (Exception ex)
        {
            // Мог отказать и сам вывод в журнал — этот путь тоже закрываем
            try { _logger.Error("power notification failed: {0}", ex); }
            catch { /* повторный throw обессмыслил бы внешний try */ }
        }

        return ERROR_SUCCESS; // возвращаем код ошибки Windows
    }

    public void Dispose()
    {
        if (_registration == IntPtr.Zero)
        {
            return;
        }

        uint result = PowerUnregisterSuspendResumeNotification(_registration);
        if (result != ERROR_SUCCESS)
        {
            // Если выбросить дескриптор, не сняв регистрацию, она останется
            // у ОС, а отслеживать её будет нечем. Когда экземпляр соберут,
            // пропадёт и _callback: ОС вызовет уже освобождённый указатель
            // на функцию. При следующей приостановке упадёт весь процесс,
            // поэтому делегат оставляем живым
            lock (AbandonedCallbacks)
            {
                AbandonedCallbacks.Add(_callback);
            }

            _logger.Error("PowerUnregisterSuspendResumeNotification failed with {0}.", result);
            return;   // раз снять не удалось, дескриптор не бросаем
        }

        _registration = IntPtr.Zero;
    }
}

В реализации важны три момента.

  • Не выпускайте исключение за границу обратного вызова. Метод вызывает напрямую ОС: если управляемое исключение пересечёт границу, на управляемой стороне некому его поймать, и падает весь процесс. Переполненный журнал или обращение к уже уничтоженному объекту — вполне реальные сбои, и тогда приложение будет умирать при каждом сне. Оберните тело в try / catch и всегда возвращайте код.
  • Обратный вызов идёт с системного потока. Окно на обработку PBT_APMSUSPEND — те же около 2 секунд, что и при доставке через окно, поэтому здесь нельзя переподключаться и делать сетевую зачистку.1718 Восстановление после возобновления тоже безопаснее отдать другому потоку по флагу.
  • Не забывайте снимать регистрацию. В PowerUnregisterSuspendResumeNotification передают дескриптор регистрации.29 Смотрите и возвращаемое значение. Если выбросить дескриптор, не сняв регистрацию, она останется у ОС, а отслеживать её будет нечем. Когда экземпляр соберут, пропадёт и делегат обратного вызова, и ОС вызовет уже освобождённый указатель на функцию.
  • Если уведомления нужны через дескриптор окна, в RegisterSuspendResumeNotification из user32.dll передают DEVICE_NOTIFY_WINDOW_HANDLE (тогда как обычно приходит WM_POWERBROADCAST).30

Если приложение делают службой Windows, можно поставить CanHandlePowerEvent у ServiceBase в true и переопределить OnPowerEvent(PowerBroadcastStatus). Те же события питания приходят через диспетчер управления службами, без P/Invoke.31

5.2. Что означают события на уровне Win32

И через окно, и через обратный вызов смысл событий один. Это стоит зафиксировать.

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

5.3. Пропуск нужно записывать как пропуск

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

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

6. Как гарантированно отработать в заданный момент — пробуждение через Планировщик заданий

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

Задание с WakeToRun в назначенный момент будит компьютер из сна или гибернации и держит систему бодрствующей до конца задания (если машина уже не спала, то же требование действует до завершения). Экран при пробуждении может оставаться выключенным — так и должно быть.20

Но пробуждение срабатывает не всегда. В официальной диагностике как условия указаны включённый параметр электропитания «Разрешить таймеры пробуждения» (Allow Wake Timer) и включённая настройка пробуждения в BIOS. На современных ноутбуках ради экономии энергии пробуждение нередко запрещают.21 Текущие активные таймеры пробуждения показывает powercfg /waketimers,12 поэтому на целевом железе обязательно проверьте, что машина действительно просыпается. Если будить нужно из своего приложения, есть таймер с пробуждением: в fResume у SetWaitableTimer передают TRUE. После автоматического пробуждения система бодрствует только в течение таймера бездействия без пользователя (минимум 2 минуты) и быстро снова уснёт, если приложение не объявит через SetThreadExecutionState, что оно «занято». Если после пробуждения работа длинная, правильное сочетание — с подавлением из главы 4.23

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

7. Таблица решений — подавлять, обрабатывать возобновление или будить

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

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

Осей две. «Есть ли окно, когда остановка допустима» (если да — Планировщик заданий, если нет — параметры электропитания) и «на чьём ПК это работает» (чем ближе приложение к личному или общему ноутбуку пользователя, тем сильнее стоит опираться на обработку возобновления, а не на подавление).

7.1. Что значит «отключить сон в параметрах электропитания»

«Отключить в параметрах электропитания» из таблицы — это конкретно следующие действия. Для ПК управления оборудованием, где сон останавливают эксплуатацией, а не приложением, гарантия появляется только когда сделано всё это.

Что сделать Где / какой командой
Время до сна — «Никогда» Параметры > Система > Питание (на ноутбуке — «Питание и батарея») > Экран и спящий режим. Либо Панель управления > Оборудование и звук > Электропитание > Настройка схемы электропитания. Командой — powercfg /change standby-timeout-ac 0 (0 значит «не уходить в сон»)1234
Пересмотреть и время до выключения экрана На том же экране «Отключать дисплей». Командой — powercfg /change monitor-timeout-ac 012
Изменить действие крышки и кнопки питания Электропитание > Выбрать действие кнопок питания > «Действие не требуется». Именно этот путь SetThreadExecutionState не перекрывает10
Проверить модель устройства powercfg /a. От S3 или Modern Standby зависят следующие оговорки3

На машине с Modern Standby нужна ещё одна оговорка. Устройство с Modern Standby уходит в ожидание, когда гаснет экран. Официальная документация как поводы называет кнопку питания, закрытие крышки, «Сон» в меню «Пуск» и ещё «когда система вышла по простою».35 Если думать в логике S3 («достаточно выключить только тайм-аут сна»), останется путь «экран погас → ожидание». Пересмотрите тайм-ауты и сна, и экрана, сверьтесь с powercfg /a и оставьте машину на ночь: она не должна остановиться. Это тоже часть процедуры.

Про батарею тоже часто забывают. standby-timeout-ac — настройка при питании от сети; от батареи действует standby-timeout-dc. Если ПК оборудования — ноутбук, в требованиях лучше явно написать питание от сети (как в главе 4, на Modern Standby от батареи сам запрос питания снимают через 5 минут).13

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 секунд, бывает сон без уведомления: сторону Resume пишите идемпотентной и записывайте интервал без данных.
  • Для запуска в заданный момент основное решение — 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 3

  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 3

  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 6 7 8 9

  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 Reset — отдельно включаемая функция; и что как мера используется 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 9

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

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

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

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

  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 3

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

  19. Microsoft Learn, PowerRegisterSuspendResumeNotification function. О том, что в Flags передают DEVICE_NOTIFY_CALLBACK, в Recipient — указатель на DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS; что в Type обратного вызова приходят PBT_APMSUSPEND / PBT_APMRESUMESUSPEND / PBT_APMRESUMEAUTOMATIC; что при успехе возвращается ERROR_SUCCESS; и что начиная с Windows 8 / Windows Server 2012 функция есть в Powrprof.dll.  2

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

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

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

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

  24. Microsoft, .NET and .NET Core Support Policy. О том, что мажорные выпуски .NET выходят каждый ноябрь, и что .NET 10 — LTS, вышедший 11 ноября 2025 года. 

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

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

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

  28. Microsoft Learn, DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS structure. О том, что в структуре два члена — Callback и Context, и что обратный вызов DeviceNotifyCallbackRoutine имеет вид ULONG(PVOID Context, ULONG Type, PVOID Setting) и возвращает код ошибки Windows. 

  29. Microsoft Learn, PowerUnregisterSuspendResumeNotification function. О снятии регистрации уведомлений по дескриптору регистрации и о том, что при успехе возвращается ERROR_SUCCESS. 

  30. Microsoft Learn, RegisterSuspendResumeNotification function. О том, что при Flags = DEVICE_NOTIFY_WINDOW_HANDLE события доставляются окну, а при DEVICE_NOTIFY_CALLBACK — обратным вызовом. 

  31. Microsoft Learn, ServiceBase.OnPowerEvent(PowerBroadcastStatus) Method. О методе, которым служба Windows принимает смену состояния питания компьютера, и о том, что переопределять его нужно при CanHandlePowerEvent = true. 

  32. Microsoft Learn, PBT_APMRESUMEAUTOMATIC event. О событии, которое доставляется при каждом возобновлении и само по себе не говорит о присутствии пользователя; при обнаружении активности пользователя следом приходит PBT_APMRESUMESUSPEND. 

  33. Microsoft Learn, PBT_APMRESUMESUSPEND event. О доставке после PBT_APMRESUMEAUTOMATIC при возобновлении по действию пользователя или при обнаружении пользователя; при удалённом пробуждении приходит только PBT_APMRESUMEAUTOMATIC. 

  34. Microsoft Learn, Sleep idle timeout. О настройке времени бездействия до автоматического сна и о том, что минимум 0 означает «не уходить в сон». 

  35. Microsoft Learn, Prepare software for modern standby. О том, что в Modern Standby система входит при выключении экрана; что поводы — кнопка питания, закрытие крышки, сон из параметров и выход системы по простою; что после фазы DAM настольные приложения не выполняются, а службы в сессии 0 ограничивают; и что запрос питания на питании от сети держит эту фазу без срока, а на DC — не дольше 5 минут. 

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

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

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

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

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

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

Об авторе

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

Го Комура

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

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

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

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