Практическое руководство: soft real-time на обычной Windows

· Обновлено: · · Разработка Windows, Soft real-time, Проектирование, Измерения

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

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

Переведён аргумент описания трассировки WPR. Утверждения статьи не менялись.
Русский текст переписан как полноценный технический перевод, а не калька с японского. Утверждения статьи не менялись.
По замечаниям ревью слишком широкие из добавленных сегодня схем переложены в вертикальную компоновку, а формулировки части схем и подписей приведены в точное соответствие с текстом. Сам текст статьи не менялся.
Добавлены 11 схем Mermaid, чтобы причинный каркас и ход чек-листа можно было проследить и по рисункам (это соответствует правилу «не меньше одной схемы на 500–750 знаков основного текста»). У существующих схем появились сквозные подписи. Сам текст статьи не менялся.
В начало статьи добавлен раздел «Карта знаний этой статьи». Это сводка понятий и связей из основного текста: краткое резюме, схема и ссылка на страницу сведений. Утверждения в тексте не менялись.
Текст обновлён по итогам внешнего ревью (1283 замечания). Содержание отдельных правок см. в записи ниже.
Таблицу «с какого раздела начинать» по диапазонам периода перенесли сразу после вывода и убрали дубли с главой 6. В начале явно разделены аудитория и языки примеров кода; добавлены объявление SetProcessInformation на C#, строки в таблице терминов, откуда брать WPR и LatencyMon и минимальные шаги. Числовые примеры явно помечены как иллюстрация чтения метрик, а не как замеры; добавлен порядок, как получить p99 на своей машине.
Первая публикация
Цитирование статьи(DOI: 10.5281/zenodo.21619639)

Статья заархивирована на Zenodo. Ниже приведены DOI, который всегда ведёт к последней версии, и DOI, закреплённый за версией, которую вы читаете.

Го Комура (2026). Практическое руководство: soft real-time на обычной Windows. KomuraSoft LLC. https://doi.org/10.5281/zenodo.21619639 https://comcomponent.com/ru/blog/2026/03/09/000-windows-soft-realtime-practical-guide-natural/

DOI (последняя версия)
10.5281/zenodo.21619639
DOI (эта версия)
10.5281/zenodo.21619640

Когда на Windows делают обработку, для которой «опоздать нельзя» — периодическую, аудио, видео, измерения, управление оборудованием, — часто складывается впечатление, что «на Windows это тяжело». Впечатление верно наполовину: Windows не hard real-time ОС, но если довести проектирование, реализацию, измерения и эксплуатацию, систему можно вывести в вполне рабочее состояние soft real-time.

В статье речь про обычную Windows 10 / 11 — без специальных расширений RTOS, своих драйверов ядра и выделенных контроллеров. Это практический разговор о том, насколько далеко можно сжать задержку и джиттер user-mode приложением на обычном настольном или портативном ПК. У аудио, видео, периодического управления и сбора данных детали разные, но узкие места во многом общие, поэтому общая часть собрана в виде чек-листа.

Для кого статья и на каком языке примеры

Текст рассчитан на разработчиков, которые на Windows делают обработку, для которой «опоздать нельзя» (периодическое управление, аудио и видео, измерения, управление оборудованием). Предполагается разработка user-mode приложений; реализация драйверов режима ядра за рамками статьи.

Языки примеров кода разделены так.

Что Язык Где
Периодический цикл, MMCSS, QoS питания и прямой вызов Win32 API C++ (Win32) 4.1, 4.3, 4.5
Те же Win32 API из C# C# (P/Invoke) 4.5
Замечания по измерению времени, GC и выделениям .NET (C#) блок «проверки для .NET» в 4.4, 5.2

Разбор причин в главах 1–3 и сам чек-лист главы 4 от языка не зависят. Если вы пишете только на C#, примеры на C++ достаточно читать как «какой API и в каком порядке вызывать».

Содержание

  1. Сначала вывод (коротко)
    • 1.1. Таблица по диапазонам периода (с какого раздела начинать)
  2. Что на обычной Windows называется «soft real-time»
    • 2.1. Что в статье значит «обычная Windows»
    • 2.2. Что получается и где начинается трудность
    • 2.3. Сначала коротко о терминах
  3. Основные причины задержки и джиттера
    • 3.1. Планировщик и приоритеты
    • 3.2. DPC / ISR и драйверы
    • 3.3. Page fault и память
    • 3.4. Разрешение таймера и управление питанием
    • 3.5. Миграция между ядрами и нагрев
  4. Практический чек-лист, как на обычной Windows уменьшить опоздания
    • 4.1. Периодический цикл и способ ожидания
    • 4.2. fast path / slow path и очередь фиксированной длины
    • 4.3. Приоритеты / MMCSS / background mode
    • 4.4. Память / GC / стоимость первого запуска
    • 4.5. Питание / EcoQoS / разрешение таймера
    • 4.6. Размещение по CPU / миграция между ядрами / нагрев
    • 4.7. Как отличить драйверы / DPC / ISR / внешние помехи
  5. Измерение и оценка
    • 5.1. Что записывать
    • 5.2. Как читать p99 / p99.9 / max
    • 5.3. Чем смотреть
    • 5.4. Как тестировать
  6. Как выбрать подход
  7. Итог
  8. Источники

Карта знаний этой статьи

Чтобы целиться в soft real-time на обычной Windows, не требуют hard real-time с гарантией нуля нарушений дедлайна, а проектируют так, чтобы задержка и jitter были малы и система не ломалась, даже если дедлайн сорван. Периодическое ожидание опирают на измерение через QueryPerformanceCounter и высокоточный waitable timer; разрешение таймера поднимают timeBeginPeriod только на нужное время. Если явно не снять понижение до EcoQoS и игнорирование разрешения таймера из-за Power Throttling, это вместе с DPC/ISR, ошибками страниц и thermal throttling становится ещё одной причиной роста jitter. В непрерывной обработке вроде звука и видео MMCSS предотвращает просрочку дедлайна приоритетным выделением ЦП; размещение по ЦП лучше сначала пробовать мягким назначением вроде CPU Sets, а не сразу жёсткой привязкой с высоким приоритетом; причину отделяют через WPR/WPA и LatencyMon.

Карта знаний: soft real-time на обычной WindowsСхема связи waitable timer и MMCSS, которые поддерживают soft real-time; связи Power Throttling и EcoQoS; путей, по которым DPC/ISR и ошибки страниц порождают jitter; ступеней от CPU Sets к привязке ЦП; и связи WPR/WPA и LatencyMon как средств измерения.используетиспользуетиспользуетнесовместимо сиспользуетпредотвращаетможет вызватьиспользуетснижаетснижаетможет вызватьне рекомендуетсяиспользуетдолжен предшествоватьможет вызватьможет вызватьможет вызватьпроверяетсяпроверяетсяпроверяетсямягкое реальное время (soft real-time)QPC (QueryPerformanceCounter)waitable timertimeBeginPeriod (запрос разрешения таймера)Power Throttling (регулировка питания)EcoQoSджиттер (jitter)MMCSS (Multimedia Class Scheduler Service)пропуск дедлайна (deadline miss)приоритет потока / класс приоритетаCPU SetsCPU affinity (привязка к процессорам)тепловой троттлингDPC/ISRошибка страницыWPR/WPA (Windows Performance Recorder/Analyzer)LatencyMon

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

1. Сначала вывод (коротко)

  • На обычной Windows цель — не гарантия hard real-time, а конфигурация soft real-time: «реже опаздывать и не ломаться, даже если опоздали».
  • Сильнее всего работает короткий, фиксированной длины и неблокирующий hot path.
  • Fast path (съём / управление) и slow path (сохранение / связь / UI) разделяют, а между ними ставят очередь фиксированной длины.
  • Периодический цикл ведут не через Sleep(1), а по абсолютному сроку.
  • Для непрерывных потоков вроде аудио и видео сначала смотрят MMCSS.
  • Время измеряют QueryPerformanceCounter (QPC); в .NET — Stopwatch.
  • Ждут событие устройства или высокоточный waitable timer.
  • timeBeginPeriod включают только на время необходимости. Не проектируют так, будто он всегда включён.
  • В реальной эксплуатации помогают питание от сети / режим питания / обращение с EcoQoS / уборка фоновой нагрузки.
  • Оценивают не только среднее, а p99 (граница, где из 100 измерений начинает быть видно самое медленное) / p99.9 / max / число пропусков / DPC / ISR / page fault / глубину очереди.

Иначе говоря, на обычной Windows эффективнее не поднимать приоритет, а срезать причины опоздания проектированием. Приоритеты и питание важны, но одни они устойчивости не создают.

Устойчивость даёт проектирование, а не приоритетСильнее всего работает короткий, фиксированной длины и неблокирующий hot path, который срезает причины опоздания ещё на уровне проекта; приоритеты и питание важны, но одни они устойчивости не создают.Hot path короткий и фиксированной длиныПричин опоздания в проекте становится меньшеРеже опаздывает и не ломается при опозданииНастройка приоритетов и питанияВажно, но одного этого мало

Рис. 1: Сильнее всего работает не настройка приоритетов, а то, что причины опоздания срезают ещё в проекте.

1.1. Таблица по диапазонам периода (с какого раздела начинать)

Статья длинная, поэтому сначала таблица, чтобы можно было читать только свою строку. Раньше этот материал стоял в конце (глава 6); его перенесли сюда.

Период и требования С какой конфигурации начинать Какие разделы читать в первую очередь
Класс 10–20 мс, редкие колебания можно поглотить Разделение fast path / slow path, очередь фиксированной длины, обычный или слегка повышенный приоритет, событийная модель. Часто этого уже хватает 4.1, 4.2
Класс 1–5 мс, нужно укладываться постоянно К предыдущему: hot path без выделений, выделенный поток, MMCSS или осторожная настройка приоритетов, высокоточный waitable timer, питание от сети и пересмотр настроек питания 4.1 — 4.5
Ближе к менее 1 мс, и не хочется промахиваться при долгой работе под нагрузкой Одним user-mode на обычной Windows это уже довольно тяжело. Сначала стоит проектировать вынос критичного участка (прошивка устройства, выделенный контроллер, FPGA, RTOS) 2.2, 6
Хочется держать вместе GUI / логи / связь / БД Не тащить всё в схему «один процесс, один цикл», а развести роли. Удобства хвоста легко ломают срок головы 4.2, 4.3, 6

Локализация причин и дисциплина измерений общие для любого диапазона (главы 3 и 5).

2. Что на обычной Windows называется «soft real-time»

2.1. Что в статье значит «обычная Windows»

Обычная Windows здесь примерно означает следующее.

  • обычный настольный или портативный ПК с Windows 10 / 11
  • без своего расширения RTOS
  • без разработки своих драйверов режима ядра
  • обычное user-mode приложение
  • настройка штатными Windows API и параметрами

То есть это не разговор про «собрать отдельную машину целиком под управление реальным временем», а про то, насколько реалистично можно сжать поведение на обычном Windows-ПК.

Обычный ПК с Windows 10 / 11User-mode приложениеЦель — soft real-timeСнизить задержкуУменьшить джиттерНаблюдать missed deadline и не ломатьсяНужна гарантия нуля нарушений срокаRTOS / выделенный контроллер / FPGA / обработка на стороне устройства

Рис. 2: User-mode приложение на обычной Windows целится в soft real-time; гарантия нуля нарушений срока — область RTOS и соседних средств.

2.2. Что получается и где начинается трудность

Даже на обычной Windows для такой обработки вполне реалистично получить состояние «редко опаздывает».

  • периодическая обработка с шагом от нескольких миллисекунд до нескольких десятков миллисекунд
  • буферный режим аудио / видео
  • съём с датчиков и контур управления
  • периодическая обработка в духе программного ПЛК
  • конвейер с низкой задержкой в отдельном от UI потоке

Но «можно» здесь не значит, что редкие всплески задержки удастся обнулить. Цель такая.

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

Наоборот, следующие требования одним user-mode на обычной Windows закрыть уже довольно тяжело.

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

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

Что даёт soft real-time и когда выносить наружуНа обычной Windows цель — низкая задержка в обычном режиме, меньший джиттер, устойчивость при редком пропуске срока и наблюдаемость этого пропуска; жёсткие требования вроде нуля нарушений срока выносят только из критичного по времени участка.Состояние, к которому идёт soft real-timeНизкая задержка в обычном режимеМеньший джиттерПри пропуске не ломается и это видноТребование нуля нарушений срокаВынести на устройство или RTOS

Рис. 3: «Можно» — не ноль всплесков, а состояние «реже опаздывает, не ломается, пропускаемое видно».

2.3. Сначала коротко о терминах

Сначала зафиксируем смысл терминов, которые встречаются в статье.

Термин Коротко Как смотреть на практике
soft real-time Редкие опоздания возможны, но их делают небольшими и неломающими Именно это в первую очередь стоит целью на обычной Windows
hard real-time Мир, где нужен ноль нарушений срока Не то, чего добиваются одним user-mode на обычной Windows
Джиттер Колебание периода или времени отклика Даже при хорошем среднем большой джиттер в эксплуатации даёт нестабильность
deadline miss Обработка не успела к запланированному времени Не прятать, а считать и писать в лог
p99 / p99.9 Показатели медленного хвоста p99 — «граница, где из 100 измерений начинает быть видно самое медленное»
DPC / ISR Обработка на стороне ядра вокруг драйверов и прерываний Если она длинная, user-mode поток ждёт
MMCSS Механизм Windows, который отдаёт CPU чувствительной ко времени обработке (аудио / видео) Сильный вариант, когда нельзя допустить опустошения буфера
QPC QueryPerformanceCounter Основа измерения прошедшего времени. Высокоточный счётчик, не настенные часы
waitable timer Объект ядра, который становится signaled в заданный момент. С флагом CREATE_WAITABLE_TIMER_HIGH_RESOLUTION у CreateWaitableTimerExW получается высокоточный вариант Лучше Sleep как основа ожидания периода (4.1)
EcoQoS Состояние «можно экономить энергию». Частоту CPU снижают или сдвигают на более экономичные ядра Для чувствительной ко времени обработки избегать. Если не сказать явно, ОС угадывает сама (4.5)
IGNORE_TIMER_RESOLUTION Указание, что запрос процесса на разрешение таймера можно игнорировать (PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION) Если включено, эффект timeBeginPeriod пропадает. В Windows 11 это иногда включается само, когда окно уходит с экрана (4.5)
CPU Sets Мягкое указание потоку или процессу «желательно работать на этой группе ядер» Пробовать раньше жёсткой привязки к конкретным ядрам (4.6)
ETW / WPR / WPA Штатная трассировка Windows (ETW), средство записи (WPR) и GUI разбора (WPA) Ими копают context switch, DPC / ISR, page fault (5.3)
LatencyMon Сторонний инструмент для задержек от драйверов По нему смотрят время ISR / DPC по драйверам и намечают, куда копать (5.3)

3. Основные причины задержки и джиттера

Причины, по которым периодическая обработка на обычной Windows опаздывает, почти всегда сводятся к одной из ветвей этой схемы.

Периодическая обработка опаздываетПланировщик / приоритетыDPC / ISR / драйверыPage fault / памятьРазрешение таймера / питаниеМиграция между ядрами / нагрев

Рис. 4: Причины опоздания периодической обработки сводятся к пяти ветвям: планировщик, DPC/ISR, память, таймер и питание, миграция ядер и нагрев.

3.1. Планировщик и приоритеты

Порядок выполнения потоков Windows задаёт приоритет. При равном приоритете потоки идут по кругу (round-robin); когда готов поток с более высоким приоритетом, поток с более низким отодвигают.

То есть даже если периодический поток написан аккуратно, раньше него вполне обычны:

  • другие потоки
  • другие процессы
  • внутренняя обработка ОС
  • продукты безопасности
  • вспомогательная обработка устройств
  • фоновая синхронизация
Как приоритетное планирование отодвигает потокПотоки с одинаковым приоритетом идут round-robin, а более высокий готовый приоритет отодвигает более низкий; поэтому перед периодическим потоком вполне обычны другие процессы и внутренняя обработка ОС.Одинаковый приоритетПо очереди, round-robinГотов более высокий приоритетБолее низкий отодвигаютДругие процессы, внутренняя обработка ОС и т. п.

Рис. 5: Даже аккуратно написанный периодический поток вполне может уступить работу с более высоким приоритетом.

3.2. DPC / ISR и драйверы

Этот момент довольно важен. Даже если приоритеты на стороне приложения приведены в порядок, пока длинные DPC (Deferred Procedure Call) или ISR (Interrupt Service Routine), user-mode поток выполняться не может.

Частыми источниками становятся такие устройства и драйверы:

  • USB
  • Wi-Fi / Bluetooth
  • хранилище
  • аудио
  • GPU
  • ACPI / питание

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

Как DPC и ISR останавливают user-modeПока длинные ISR и DPC устройств вроде USB, Wi-Fi и GPU идут в ядре, user-mode поток не выполняется; поднять приоритет приложения недостаточно, чтобы это выиграть.Устройства вроде USB, Wi-Fi, GPUISR и DPC идут в ядреПока они идут, user-mode не выполняетсяПовышение приоритета это не выигрывает

Рис. 6: Даже если код приложения не виноват, user-mode могут остановить драйвер или железо.

3.3. Page fault и память

Если в hot path случается page fault (нужной страницы нет в памяти, её приходится подгружать), задержка резко растёт.

Особенно стоит избегать таких паттернов:

  • commit страницы при первом обращении
  • отложенная загрузка
  • page-in memory-mapped файлов
  • динамические выделения сверх необходимого
  • большие объекты или фрагментированная куча

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

Задержка из-за page fault в hot pathЕсли страница, к которой обратились в hot path, не в памяти, её подгружают через page fault и задержка резко растёт; поэтому нужную память выделяют заранее и один раз касаются при запуске.данетHot path обращается к памятиСтраница уже в памяти?Идём дальшеПодгрузка через page faultЗадержка резко растётВыделить заранее и один раз коснуться при запуске

Рис. 7: Задержка скачет в зависимости от того, случился page fault или нет. Мера — предварительное выделение и прогрев при запуске.

3.4. Разрешение таймера и управление питанием

«Хочу раз в 1 мс, поэтому Sleep(1)» почти никогда не срабатывает. Точность ожидания в Windows зависит от разрешения таймера, планирования и состояния питания.

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

3.5. Миграция между ядрами и нагрев

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

При долгой работе нельзя игнорировать и нагрев. Когда включается thermal throttling, ранее стабильный период может разъехаться.

Как миграция ядер и нагрев ломают периодМиграция потока между ядрами заставляет кеш прогреваться заново и при высокой нагрузке даёт колебания; при долгой работе нагрев включает thermal throttling и ранее стабильный период разъезжается.Поток мигрирует между ядрамиКеш прогревается зановоПри высокой нагрузке это источник колебанийПри долгой работе копится теплоThermal throttlingРанее стабильный период разъезжается

Рис. 8: Миграция ядер бьёт джиттером, нагрев — троттлингом; оба съедают устойчивость долгой работы.

4. Практический чек-лист, как на обычной Windows уменьшить опоздания

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

4.1. Периодический цикл и способ ожидания

Сначала типичный антипаттерн.

while (running)
{
    Sleep(1);
    Step();
}

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

От абсолютного срокаОт относительного времениWaitUntil(next - margin)next += periodПри необходимости короткий spinFastStep()Step()Sleep(1)Ошибка ожидания и время выполнения понемногу копитсяДрейф копится хуже

Рис. 9: Цикл от Sleep(1) копит ошибку; цикл по абсолютному сроку дрейф копит хуже.

Чек-лист

  • Периодический цикл не построен на Sleep(1)
  • Период ведут по абсолютному сроку next += period
  • Для ожидания предпочитают событие устройства или waitable timer
  • Только финальную подстройку ограничивают очень коротким busy-spin
  • timeBeginPeriod включают только на время необходимости и потом возвращают
  • Поведение проверено и в свёрнутом / скрытом / невидимом состоянии

Периодический цикл стабильнее вести не от относительного времени, а по абсолютному сроку.

int64_t next = QpcNow() + periodTicks;

while (running)
{
    WaitUntil(next - wakeMarginTicks);

    while (QpcNow() < next)
    {
        CpuRelax(); // Короткий spin только в самом конце
    }

    int64_t started = QpcNow();
    FastStep();
    int64_t finished = QpcNow();

    RecordTiming(next, started, finished);

    next += periodTicks;

    while (finished > next)
    {
        ++missedDeadlines;
        next += periodTicks;
    }
}

4.2. fast path / slow path и очередь фиксированной длины

Основа архитектуры — в fast path оставлять только работу, чувствительную к сроку, остальное выносить в slow path.

Устройство / события съёмаfast path: съём, управление, минимум копированияОчередь фиксированной длиныslow path: сохранение, отправка, UI, агрегацияЗапись lateness / miss / глубины очереди

Рис. 10: В fast path оставляют только работу, чувствительную к сроку, и через очередь фиксированной длины выносят её в slow path.

В fast path оставляют примерно это.

  • съём данных
  • расчёт управляющих значений
  • минимально необходимое копирование
  • временная метка
  • постановка в очередь
  • запись miss / overrun

Всё остальное сбрасывают в slow path.

Чек-лист

  • В hot path нет записи в файл, сетевой отправки, записи в БД
  • В hot path нет тяжёлого логирования, Flush, синхронного RPC
  • fast path и slow path явно разведены по потокам или ролям
  • Очередь фиксированной длины
  • Политика при переполнении очереди решена заранее
  • Наблюдают число miss, число drop и глубину очереди
  • Обновление UI и агрегация логов вынесены в более редкий цикл

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

Важно последнее значениеВажна каждая записьДля логовОчередь заполненаЧто бережём?Выбросить старое, оставить последнееОповещение / остановка / управление вверх по потокуВыбросить старое, фиксировать только число drop

Рис. 11: Что беречь при заполненной очереди, решают заранее — в зависимости от назначения.

4.3. Приоритеты / MMCSS / background mode

Базовое правило приоритетов — не поднимать всё подряд. На обычной Windows лучше работает схема «поднять только важный поток, фоновую работу честно опустить». Background mode — механизм, который склоняет к низкому приоритету не только CPU, но и такие ресурсы, как I/O.

Разделить работуПотоки, чувствительные к срокуСохранение / отправка / сжатие / агрегацияUIПри необходимости повышенный приоритет или MMCSSbackground mode / пониженный приоритетОбычный приоритетНе начинать сразу с REALTIME_PRIORITY_CLASS

Рис. 12: Как распределять приоритеты: поднимают только чувствительные к сроку потоки, фоновую работу честно опускают.

Чек-лист

  • Не все потоки стоят на высоком приоритете
  • Подняты только потоки, которым время действительно критично
  • Фоновая работа — сохранение, отправка, сжатие, синхронизация — уведена в background mode
  • Для непрерывной буферной обработки (аудио, видео, захват, воспроизведение) рассматривается MMCSS
  • Мыслят сначала потоками, а не процессом целиком
  • REALTIME_PRIORITY_CLASS не используют, пока необходимость не стала явной

MMCSS (Multimedia Class Scheduler Service) особенно полезен для обработки вроде аудио / видео, где нужно за фиксированное время заполнить буфер. Это лучше согласуется с устройством Windows, чем постоянно крутить поток с высоким приоритетом.

По коду это выглядит примерно так.

DWORD taskIndex = 0;
HANDLE avrt = AvSetMmThreadCharacteristicsW(L"Pro Audio", &taskIndex);
if (!avrt)
{
    throw std::runtime_error("AvSetMmThreadCharacteristicsW failed");
}

// Крутим цикл, чувствительный ко времени

if (!AvRevertMmThreadCharacteristics(avrt))
{
    throw std::runtime_error("AvRevertMmThreadCharacteristics failed");
}
Регистрация потока в MMCSS и снятиеПеред чувствительным ко времени циклом поток регистрируют в MMCSS через AvSetMmThreadCharacteristicsW и после цикла возвращают через AvRevertMmThreadCharacteristics; MMCSS отдаёт CPU чувствительной ко времени обработке.AvSetMmThreadCharacteristicsWКрутим цикл, чувствительный ко времениAvRevertMmThreadCharacteristicsMMCSS отдаёт CPU в приоритетном порядке

Рис. 13: MMCSS берут парой регистрация / снятие. Для непрерывной буферной обработки это ближе к устройству Windows, чем постоянно высокий приоритет.

4.4. Память / GC / стоимость первого запуска

Если в hot path на каждом шаге делать new / malloc / List<T>.Add / склейку строк / LINQ, рано или поздно наружу вылезут сборка мусора и переезд блоков. Сама GC не зло, но код с частыми выделениями проявляет это как джиттер.

ЗапускВыделить нужные буферыОдин раз коснуться, чтобы прогреть страницыЗакрыть JIT / загрузку DLL / первый I/OПотом — боевые измерения / боевая работа

Рис. 14: При запуске выделяют буферы, прогревают страницы и закрывают стоимость первого запуска — и только потом идут в боевые измерения и работу.

Чек-лист

  • В hot path нет выделения / освобождения памяти на каждом шаге
  • Нужные буферы выделены заранее при запуске
  • При запуске страницы один раз прогреты обращением
  • Первый JIT, первая загрузка DLL, первый I/O не попадают в боевые измерения
  • В цикле не растут огромные структуры или логи переменной длины
  • Если VirtualLock всё же используется, то только на очень маленькой критической области

Проверки для .NET

  • Время измеряют Stopwatch / Stopwatch.GetTimestamp()
  • В hot path нет LINQ, склейки строк, ToString(), генерации огромных логов
  • async/await не заносят в hot path
  • Оценку до и после прогрева ведут раздельно

4.5. Питание / EcoQoS / разрешение таймера

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

Питание на обычной WindowsРаботать от сетиРежим питания: ближе к максимальной производительностиПри необходимости — отдельный план питания для бояЧувствительные ко времени процессы держать вне EcoQoSПроверить, как обрабатывают запрос на разрешение таймера

Рис. 15: Что проверять вокруг питания: источник, режим, EcoQoS, обращение с запросом на разрешение таймера.

Чек-лист

  • Боевую оценку сначала ведут при питании от сети
  • [Параметры] > [Система] > [Питание и батарея] > [Режим питания] стоит ближе к максимальной производительности
  • Во время работы не включены battery saver / режим с приоритетом экономии
  • Проверены фирменные утилиты производителя — тихий / eco / battery-приоритетный режимы
  • Чувствительные ко времени процессы ненароком не уведены в EcoQoS (QoS с уклоном в экономию)
  • IGNORE_TIMER_RESOLUTION не включён у чувствительного ко времени процесса
  • Проверено, не меняется ли действие запроса на разрешение таймера при сворачивании / скрытии окна
  • Настройки питания для повседневной работы отделены от боевых / измерительных / демонстрационных

timeBeginPeriod полезен, если пользоваться им аккуратно, но это не панацея.

  • вызывать непосредственно перед необходимостью
  • по окончании возвращать через timeEndPeriod
  • начиная с Windows 10 version 2004 поведение уже не настолько глобальное, как раньше
  • в Windows 11 у процесса с окном, которое полностью скрыто / свёрнуто / не видно / не слышно, высокое разрешение может не гарантироваться
  • повышение разрешения не поднимает точность QPC

Если подозреваете влияние питания или QoS, состояние power throttling проверяют через SetProcessInformation.

PROCESS_POWER_THROTTLING_STATE state{};
state.Version = PROCESS_POWER_THROTTLING_CURRENT_VERSION;
state.ControlMask =
    PROCESS_POWER_THROTTLING_EXECUTION_SPEED |
    PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION;
state.StateMask = 0; // HighQoS (ближе к производительности) + уважать запрос на разрешение таймера

if (!SetProcessInformation(
        GetCurrentProcess(),
        ProcessPowerThrottling,
        &state,
        sizeof(state)))
{
    throw std::runtime_error("SetProcessInformation failed");
}

ControlMask — «какими механизмами управляю сам», StateMask — «включить этот механизм или выключить». В примере выбраны оба механизма, и оба выключены. То есть это заявление: не уходить в EcoQoS (ближе к HighQoS) и не давать игнорировать запрос на разрешение таймера. Если поставить ControlMask в 0, оба снова отдаются ОС (поведение по умолчанию).

Роль двух масок в настройке power throttlingControlMask выбирает механизмы, которыми управляете сами, StateMask включает или выключает их; так можно заявить, что процесс не уходит в EcoQoS и что запрос на разрешение таймера нельзя игнорировать.ControlMask выбирает, чем управлятьStateMask задаёт on и offЗаявление не уходить в EcoQoSНе давать игнорировать запрос на разрешение таймераControlMask = 0 возвращает решение ОС

Рис. 16: Роль двух масок SetProcessInformation. Сначала выбирают, чем управлять, потом заявляют on / off.

Вызов из C#

То же самое из C# объявляют через P/Invoke так.

// C# / .NET 8
using System.Runtime.InteropServices;

internal static class PowerQos
{
    [StructLayout(LayoutKind.Sequential)]
    private struct PROCESS_POWER_THROTTLING_STATE
    {
        public uint Version;
        public uint ControlMask;
        public uint StateMask;
    }

    private const uint PROCESS_POWER_THROTTLING_CURRENT_VERSION = 1;
    private const uint PROCESS_POWER_THROTTLING_EXECUTION_SPEED = 0x1;
    private const uint PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION = 0x4;

    // ProcessPowerThrottling — 5-й элемент PROCESS_INFORMATION_CLASS (с нуля это 4)
    private const int ProcessPowerThrottling = 4;

    [DllImport("kernel32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool SetProcessInformation(
        IntPtr hProcess,
        int processInformationClass,
        ref PROCESS_POWER_THROTTLING_STATE processInformation,
        uint processInformationSize);

    [DllImport("kernel32.dll")]
    private static extern IntPtr GetCurrentProcess();

    /// <summary>Снимает и энергосберегающее обращение, и игнорирование запроса на разрешение таймера.</summary>
    public static void OptOutOfPowerThrottling()
    {
        var state = new PROCESS_POWER_THROTTLING_STATE
        {
            Version = PROCESS_POWER_THROTTLING_CURRENT_VERSION,
            ControlMask =
                PROCESS_POWER_THROTTLING_EXECUTION_SPEED |
                PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION,
            StateMask = 0,
        };

        if (!SetProcessInformation(
                GetCurrentProcess(),
                ProcessPowerThrottling,
                ref state,
                (uint)Marshal.SizeOf<PROCESS_POWER_THROTTLING_STATE>()))
        {
            throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
        }
    }
}

Вызов: один раз PowerQos.OptOutOfPowerThrottling(); до старта чувствительного ко времени цикла. У handle процесса нужны права PROCESS_SET_INFORMATION; псевдо-handle своего процесса от GetCurrentProcess() этого достаточно.

4.6. Размещение по CPU / миграция между ядрами / нагрев

Размещение по CPU чаще лучше начинать не с резкой привязки к конкретным ядрам (hard affinity / CPU pinning), а с указания, близкого к soft affinity: «желательно работать преимущественно на этой группе ядер».

данетСначала измеритьSetThreadIdealProcessor / CPU SetsУлучшение достаточное?Остановиться на этомВ последнюю очередь рассмотреть SetThreadAffinityMaskПараллельно смотреть температуру / частоту / долгую работу

Рис. 17: Размещение по CPU начинают с измерений, мягкого указания хватает — на нём останавливаются; привязка к конкретным ядрам — крайняя мера.

Чек-лист

  • Размещение по CPU меняют только после измерения
  • Нет резкой привязки к конкретным ядрам с самого начала
  • Сначала пробуют SetThreadIdealProcessor или CPU Sets
  • SetThreadAffinityMask рассматривают как крайнюю меру
  • При долгой работе смотрят температуру, частоту, thermal throttling
  • Проверены тихий режим и режим пониженного шума у портативных ПК

Порядок безопаснее такой.

  1. сначала измерить
  2. при необходимости — ideal processor / CPU Sets
  3. если улучшение всё ещё нужно — привязка к конкретным ядрам

Привязка к конкретным ядрам выглядит сильной, но она сужает запасные пути ОС, поэтому при неосторожном использовании система, наоборот, становится менее гибкой.

4.7. Как отличить драйверы / DPC / ISR / внешние помехи

Когда «иногда взрывается только max» или «среднее хорошее, а p99.9 плохой», стоит подозревать и помехи за пределами кода приложения.

данетданетданетданетПоявился всплеск late / miss / maxСвоё время обработки тоже велико?Укоротить hot path / урезать выделения / убрать I/OЕсть всплески DPC / ISR?Проверить USB / Wi-Fi / Bluetooth / GPU / аудио / хранилище / ACPI / обновления драйверовЕсть page fault / GC / стоимость первого запуска?Предварительное выделение / прогрев / снизить нагрузку на кучуЕсть влияние батареи / экономии / нагрева?Питание от сети / настройки питания / охлаждение / длинные тестыКопать глубже через ETW / WPA / LatencyMon

Рис. 18: Порядок, в котором копают всплеск. Сначала своя обработка, затем DPC/ISR, память, питание и нагрев.

Чек-лист

  • Проверены драйверы вокруг Wi-Fi / Bluetooth / USB / хранилища / GPU / аудио
  • Сравнили с отключёнными ненужной облачной синхронизацией, индексированием, автообновлениями
  • Проверили, разъезжается ли всё при сворачивании окна или выключении экрана
  • Тенденции DPC / ISR смотрели через LatencyMon или ETW
  • Раздельно смотрели, «моя обработка тяжёлая» или «меня останавливают снаружи»

5. Измерение и оценка

5.1. Что записывать

Как минимум стоит фиксировать следующее.

  • запланированное время цикла
  • фактическое время начала
  • фактическое время окончания
  • lateness (насколько позже запланированного начала фактически стартовали)
  • время выполнения
  • число missed deadline
  • число подряд идущих missed deadline
  • глубину очереди
  • число drop
  • загрузку CPU
  • перекос по ядрам
  • всплески DPC / ISR
  • page fault
  • колебания температуры / частоты

Одно среднее суть почти не показывает. В бою мешают как раз редкие крупные всплески задержки.

5.2. Как читать p99 / p99.9 / max

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

Показатель Смысл Как это выглядит на 10 000 измерений
Среднее Сглаженное общее значение Всплески легко тонут
p50 Середина Близко к обычному ощущению
p95 Граница, где начинают быть видны самые медленные 5% Граница без самых медленных 500
p99 Граница, где начинает быть видно самое медленное 1% Граница без самых медленных 100
p99.9 Граница, где начинают быть видны самые медленные 0,1% Граница без самых медленных 10
max Худший случай Единственный самый медленный результат

Например, пусть получился такой ряд (это пример, как читать показатели, а не замер на конкретном железе).

  • среднее: 0,8 мс
  • p99: 1,2 мс
  • p99.9: 3,5 мс
  • max: 28 мс

Это состояние «обычно быстро, но иногда бывает крупный всплеск». На обычной Windows настоящая проблема почти всегда вылезает в этом хвосте от p99 до max.

Чем среднее отличается от показателей хвостаОдно среднее прячет редкие крупные задержки; p99, p99.9 и max показывают медленный хвост, и на обычной Windows настоящая проблема как раз там.Смотреть только среднееРедкие крупные задержки прячутсяСмотреть p99, p99.9, maxВиден медленный хвостНастоящая проблема вылезает от p99 до max

Рис. 19: Хорошее среднее при скачущем max — это «обычно быстро, иногда скачет». Хвост ловят отдельными показателями.

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

Минимальный порядок, как получить p99 на своей машине

  1. В hot path только записывают. Stopwatch.GetTimestamp() (в C++ — QueryPerformanceCounter) снимает lateness и время выполнения в заранее выделенный массив. Среднее и сортировку здесь считать нельзя
  2. Останавливают измерение и только потом сводят. Сортируют и берут значение в позиции процентиля
  3. Тот же порядок повторяют при смене условий. До и после прогрева, сеть / батарея, UI на переднем плане / свёрнут, с нагрузкой других процессов и без (5.4)
  4. После каждого одиночного изменения снимают заново в тех же условиях и сравнивают
// C# / .NET 8. Сводку считают после остановки измерения
using System.Diagnostics;

// В hot path только пишем в массив (ноль выделений)
long[] latenessTicks = new long[100_000];
int count = 0;

// Пример внутри периодического цикла
// latenessTicks[count++] = Stopwatch.GetTimestamp() - scheduledTimestamp;

static double PercentileMs(long[] ticks, int count, double percentile)
{
    long[] sorted = ticks.AsSpan(0, count).ToArray();
    Array.Sort(sorted);

    int index = (int)Math.Ceiling(percentile / 100.0 * count) - 1;
    index = Math.Clamp(index, 0, count - 1);

    return sorted[index] * 1000.0 / Stopwatch.Frequency;
}

// Как пользоваться
// Console.WriteLine($"p50={PercentileMs(latenessTicks, count, 50):F3}ms");
// Console.WriteLine($"p99={PercentileMs(latenessTicks, count, 99):F3}ms");
// Console.WriteLine($"p99.9={PercentileMs(latenessTicks, count, 99.9):F3}ms");
// Console.WriteLine($"max={PercentileMs(latenessTicks, count, 100):F3}ms");

Stopwatch.Frequency — число отсчётов в секунду, поэтому после деления умножение на 1000 даёт миллисекунды. На малом числе выборок p99.9 бессмыслен. Если говорите про p99.9, соберите минимум 10 000 выборок (лучше 100 000).

5.3. Чем смотреть

Набор инструментов в целом известен.

  • Измерение внутри приложения Сначала самим снять period / lateness / execution time / queue depth / drop
  • ETW / WPR / WPA Копать CPU, context switch, DPC / ISR, page fault
  • LatencyMon Наметить колебания от драйверов
  • Мониторинг температуры / частоты Смотреть влияние нагрева
Измерение внутри приложенияp50 / p95 / p99 / p99.9 / maxmiss / drop / глубина очередиETW / WPR / WPAcontext switch / DPC / ISR / page faultМониторинг температуры / частотыРешить, что улучшать в первую очередь

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

Дойти до WPA несколько трудозатратно, но это весьма помогает отличить, виноваты DPC / ISR или просто своя обработка слишком тяжёлая.

Откуда брать и как пользоваться по минимуму

Инструмент Откуда взять Минимальные шаги
WPR / WPA (Windows Performance Toolkit) При установке Windows ADK (Windows Assessment and Deployment Kit) отмечают «Windows Performance Toolkit». Каталог по умолчанию: C:\Program Files (x86)\Windows Kits\10\Windows Performance Toolkit Командную строку от администратора: (1) wpr -start CPU — начать запись, (2) десятки секунд прогнать проблемную обработку, (3) wpr -stop trace.etl "исследование периодических задержек" — сохранить. Дальше открыть trace.etl в WPA. Имена доступных профилей смотрят wpr -profiles
LatencyMon Скачивают с сайта Resplendence Software. Для личного использования есть бесплатная Home Edition Запускают измерение и несколько минут крутят целевую обработку. Сводятся максимальная задержка таймера ядра, время ISR / DPC по драйверам и hard pagefault; драйверы с выделяющимся временем выполнения записывают

В WPA сначала смотрят среди графиков CPU время выполнения DPC / ISR и context switch. Там видно, какой драйвер держал CPU в те промежутки, когда ваш поток не мог работать; это сверяют с моментами задержки из 5.1.

Скриншотов экрана в статье нет. Внешний вид плавает от версии, поэтому пройдите шаги выше и смотрите на своей машине.

5.4. Как тестировать

Одного тихого стенда недостаточно. Как минимум эти условия стоит смотреть раздельно.

  • сразу после запуска, до прогрева
  • после прогрева
  • долгая непрерывная работа
  • UI на переднем плане
  • UI свёрнут / состояние, близкое к скрытому
  • питание от сети
  • питание от батареи
  • нагрузка на сеть или диск

Оценка только на стенде легко пропускает проблемы, которые вылезают в эксплуатации. Обычная Windows легко тянется за тем, как ею пользуются, поэтому важно проверять ближе к реальным условиям.

Зачем разводить условия тестаОценка только на тихом стенде пропускает эксплуатационные проблемы, поэтому условия смотрят раздельно: до и после прогрева, долгая работа, UI на переднем плане и свёрнутый, питание от сети и от батареи, есть нагрузка или нет.Оценка только на тихом стендеЭксплуатационные проблемы пропускаютсяОценивать по отдельным условиямПрогрев и долгая работаUI на переднем плане и свёрнутыйПитание и нагрузка

Рис. 21: Обычная Windows тянется за тем, как ею пользуются. Оценку приближают к реальным условиям.

6. Как выбрать подход

Таблицу по диапазонам периода перенесли в 1.1, чтобы к ней можно было вернуться по ходу чтения. Здесь — два решения, которых одной таблицей не закрыть.

Когда решать, что «на обычной Windows уже не вытянуть»

Если нужно много часов под нагрузкой держать менее 1 мс, часто быстрее не продолжать крутить настройки, а вынести только жёсткий по времени участок. Кандидаты: прошивка устройства, выделенный контроллер, FPGA, RTOS. Решение опирают не на ощущение, а на цифры главы 5.

  • hot path уже нельзя укоротить, а p99.9 и max всё равно выше требования
  • виноваты внешние помехи (DPC / ISR, драйверы, другие процессы), и мерами на стороне приложения уже не достать (это подтверждают разбором из 4.7)
  • требование уже не «если иногда сорвём — не ломаемся», а «нельзя сорвать ни разу»

Когда хочется держать всё в одном процессе

Если GUI, логи, связь и БД живут в одном цикле одного процесса, удобства хвоста ломают срок головы. Ожидание flush файла, переподключение БД, перерисовка UI легко растягиваются на десятки миллисекунд. Разделение fast path / slow path (4.2) можно расширить и до границ процессов. Если процессы разведены, голова продолжает крутить период, даже когда хвост завис.

Совместное проживание против разведения по процессамЕсли GUI, логи, связь и БД живут в одном цикле одного процесса, удобства хвоста ломают срок головы; если разделение fast path и slow path довести до процессов, голова продолжает крутить период, даже когда хвост завис.Всё в одном процессе и одном циклеУдобства хвоста ломают срок головыОжидание flush, переподключение, перерисовкаГолову и хвост развести по процессамХвост завис — голова продолжает крутиться

Рис. 22: Если совместное проживание всё же нужно, разделение fast path / slow path можно довести до границ процессов.

7. Итог

Две предпосылки стоит зафиксировать.

  • На обычной Windows цель — не гарантия hard real-time, а конфигурация soft real-time: меньшая задержка и джиттер и отсутствие поломки при нарушении срока
  • Сильнее всего работает не настройка приоритетов, а наведение порядка в hot path

На стороне реализации работают такие вещи.

  • разделить fast path и slow path
  • очередь фиксированной длины и заранее решённая политика при переполнении
  • измерять через QPC, ждать через событие / waitable timer
  • в hot path избегать выделений, блокирующего I/O, тяжёлых блокировок

На стороне эксплуатации работают такие вещи.

  • работать от сети
  • держать отдельные настройки питания для боя
  • убирать ненужную фоновую нагрузку
  • оценивать по p99 / p99.9 / max и числу пропусков

Soft real-time на обычной Windows не определяется одними приоритетами: если раздельно довести проектирование, реализацию, питание, измерения и эксплуатацию, система может стать довольно устойчивой.

8. Источники

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

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

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

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

Можно ли делать обработку реального времени на Windows?
Гарантировать hard real-time (ноль нарушений срока) нельзя. Но если довести проектирование, реализацию, измерения и эксплуатацию, систему можно вывести в вполне рабочее состояние soft real-time. Периодическая обработка с шагом от нескольких миллисекунд до нескольких десятков миллисекунд, буферный режим для аудио/видео, съём с датчиков и контур управления на обычной Windows 10/11 реалистичны. Если же нужна гарантия нуля нарушений срока или многочасовая устойчивость ниже нескольких сотен микросекунд, стоит смотреть в сторону RTOS, выделенного контроллера, FPGA или обработки на стороне устройства.
Почему в периодической обработке нельзя опираться на Sleep(1)?
Потому что Sleep(1) даёт не «период 1 мс», а «подождать примерно не меньше 1 мс и сверху добавить время обработки»; перерасход ожидания накапливается как есть. Цикл ведут по абсолютному сроку next += period, ждут событие устройства или высокоточный waitable timer, а короткий busy-spin оставляют только на финальную подстройку. timeBeginPeriod включают лишь на время необходимости и потом возвращают.
Что сильнее всего снижает задержку и джиттер?
Не повышение приоритета, а короткий, фиксированной длины и неблокирующий hot path. Fast path (съём / управление) и slow path (сохранение / связь / UI) разделяют и соединяют очередью фиксированной длины; в hot path не пишут в файл, не отправляют в сеть, не делают тяжёлое логирование и не выделяют память на каждом шаге. В эксплуатации помогают питание от сети, пересмотр режима питания, проверка EcoQoS и уборка лишней фоновой нагрузки.
По каким показателям оценивать устойчивость периодической обработки?
Не только по среднему, а по p99, p99.9, max и числу missed deadline. Например, среднее 0,8 мс при max 28 мс значит: обычно быстро, но иногда бывает крупный всплеск. На обычной Windows настоящая проблема как раз в этом хвосте от p99 до max. Параллельно фиксируют всплески DPC/ISR, page fault, глубину очереди, колебания температуры и частоты и оценивают по отдельным условиям: до и после прогрева, при долгой работе, в свёрнутом состоянии, от батареи и т. д.

Об авторе

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

Го Комура

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

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

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

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