Практическое руководство по максимальному приближению к мягкому реальному времени на обычной Windows
· Го Комура · Разработка Windows, Мягкое реальное время, Проектирование, Измерения
Когда на Windows создают процессы, для которых «задержка — это проблема», — периодическую обработку, обработку аудио, обработку видео, измерения, управление оборудованием, — часто складывается впечатление, что «на Windows это тяжело». Это впечатление наполовину верно, а наполовину нет: Windows не является ОС hard real-time, но если тщательно проработать проектирование, реализацию, измерения и эксплуатацию, систему можно довести до вполне практичного состояния soft real-time.
В этой статье речь идёт об обычной Windows 10 / 11 — без специальных расширений RTOS, собственных драйверов ядра или выделенных контроллеров. Это практический разговор о том, насколько далеко можно продвинуться в снижении задержки и джиттера с помощью пользовательского (user-mode) приложения на обычном настольном или портативном ПК. У аудио, видео, периодического управления и сбора данных детали различаются, но проблемные места во многом общие, поэтому на этот раз эта общая часть собрана в виде чек-листа.
Содержание
- Сначала вывод (в двух словах)
- Что значит «мягкое реальное время» на обычной Windows
- 2.1. Что в этой статье понимается под «обычной Windows»
- 2.2. Что достижимо, а где начинаются трудности
- 2.3. Сначала — коротко о терминах
- Основные причины задержки и джиттера
- 3.1. Планировщик и приоритеты
- 3.2. DPC / ISR и драйверы
- 3.3. Page fault и память
- 3.4. Разрешение таймера и управление питанием
- 3.5. Миграция между ядрами и нагрев
- Практический чек-лист снижения задержек на обычной 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.1. Что записывать
- 5.2. Как читать p99 / p99.9 / max
- 5.3. Чем измерять
- 5.4. Дисциплина тестирования
- Краткое руководство по выбору
- Итог
- Источники
1. Сначала вывод (в двух словах)
- На обычной Windows цель — не гарантия hard real-time, а конфигурация soft real-time: «маловероятно опоздать, и не сломаться, даже если опоздали»
- Самый большой эффект даёт то, что горячий путь становится коротким, фиксированной длины и неблокирующим
- Fast path (получение / управление) и slow path (сохранение / связь / UI) разделяют, а между ними ставят очередь фиксированной длины
- Периодический цикл прогоняют не полагаясь на
Sleep(1), а по абсолютному сроку - Для непрерывных потоков вроде аудио и видео в первую очередь рассматривают MMCSS
- Для измерения времени используют QueryPerformanceCounter (QPC), а в .NET —
Stopwatch - Для ожидания предпочитают события устройства или высокоточный waitable timer
timeBeginPeriodиспользуют только на время реальной необходимости. Не проектируют исходя из того, что он включён постоянно- В реальной эксплуатации помогают питание от сети переменного тока / режим питания / обращение с EcoQoS / наведение порядка в фоновой нагрузке
- Оценивают не только по среднему значению, а по p99 (граница, где начинает быть видно самое медленное 1 измерение из 100) / p99.9 / max / числу пропусков / DPC / ISR / page fault / глубине очереди
Иными словами, на обычной Windows эффективнее не повышение приоритета, а сокращение причин задержки средствами проектирования. Приоритеты и настройки питания важны, но одни они стабильности не создают.
2. Что значит «мягкое реальное время» на обычной Windows
2.1. Что в этой статье понимается под «обычной Windows»
Под обычной Windows здесь примерно подразумевается следующее.
- обычный настольный или портативный ПК с Windows 10 / 11
- без собственных расширений RTOS
- без разработки собственных драйверов режима ядра
- обычное user-mode приложение
- настройка с помощью стандартных Windows API и параметров
То есть речь идёт не о том, чтобы «полностью собрать отдельную машину под управление реальным временем», а о том, насколько реалистично можно продвинуться на обычном Windows-ПК.
flowchart LR
A["Обычный ПК с Windows 10 / 11"] --> B["Приложение user-mode"]
B --> C["Цель — мягкое реальное время"]
C --> D["Снизить задержку"]
C --> E["Уменьшить джиттер"]
C --> F["Наблюдать пропуски сроков и не ломаться"]
G["Нужна гарантия нулевых нарушений сроков"] -.-> H["RTOS / выделенный контроллер / FPGA / обработка на стороне устройства"]
2.2. Что достижимо, а где начинаются трудности
Даже на обычной Windows, например, для такой обработки можно вполне реалистично создать состояние «редко опаздывает».
- периодическая обработка с интервалом от нескольких миллисекунд до нескольких десятков миллисекунд
- буферный режим аудио / видео
- сбор данных с датчиков и циклы управления
- периодическая обработка в духе программного ПЛК
- конвейер с низкой задержкой, работающий в отдельном от UI потоке
При этом «можно» здесь не означает, что редкие всплески задержки удастся полностью свести к нулю — цель, скорее, состоит в следующем.
- снизить задержку в обычном режиме
- уменьшить джиттер
- не ломаться, даже если срок иногда пропущен
- уметь наблюдать сам факт пропуска
И наоборот, следующие требования выполнить одним лишь user-mode на обычной Windows довольно тяжело.
- гарантировать нулевые нарушения сроков
- стабильно выдерживать интервалы менее нескольких сотен микросекунд в течение долгого времени
- сосуществовать с тяжёлым GUI, сетью и хранилищем
- делать это при работе от батареи или с приоритетом энергосбережения
- не допускать даже всплесков, вызванных драйверами или устройствами
В таких случаях безопаснее также рассмотреть перенос только действительно критичных по времени участков на firmware устройства, выделенный контроллер, FPGA или RTOS.
2.3. Сначала — коротко о терминах
Сначала зафиксируем значение терминов, которые встречаются в этой статье.
| Термин | Коротко | Практический взгляд |
|---|---|---|
| soft real-time (мягкое реальное время) | Подход, при котором редкие задержки допустимы, но их стараются сделать небольшими и не ломающими систему | Именно это в первую очередь стоит выбирать целью на обычной Windows |
| hard real-time (жёсткое реальное время) | Мир, где нужно гарантировать нулевые нарушения сроков | Не то, чего стоит добиваться одним лишь user-mode на обычной Windows |
| Джиттер | Колебание периода или времени отклика | Даже при хорошем среднем значении большой джиттер означает нестабильность в реальной эксплуатации |
| Пропуск срока (deadline miss) | Обработка не завершилась к запланированному времени | Не скрывать, а считать и записывать в лог |
| p99 / p99.9 | Показатели для взгляда на медленный «хвост» | p99 — это «граница, где начинает быть видно самое медленное 1 измерение из 100» |
| DPC / ISR | Обработка на стороне ядра вокруг драйверов и прерываний | Если она длинная, user-mode-поток вынужден ждать |
| MMCSS | Механизм Windows, распределяющий CPU для чувствительной ко времени обработки (аудио / видео) | Сильный вариант для обработки, которой нельзя допускать опустошения буфера |
| QPC | QueryPerformanceCounter |
Основа измерения прошедшего времени. Высокоточный счётчик, а не настенные часы |
3. Основные причины задержки и джиттера
Причины задержки периодической обработки на обычной Windows почти всегда сводятся к одному из пунктов этой схемы.
flowchart TD
Late["Периодическая обработка запаздывает"] --> S["Планировщик / приоритеты"]
Late --> D["DPC / ISR / драйверы"]
Late --> M["Page fault / память"]
Late --> T["Разрешение таймера / управление питанием"]
Late --> C["Миграция между ядрами / нагрев"]
3.1. Планировщик и приоритеты
Порядок выполнения потоков Windows определяется приоритетом. При равном приоритете потоки идут по кругу (round-robin), а когда становится готовым к выполнению поток с более высоким приоритетом, поток с более низким приоритетом отодвигается в сторону.
То есть даже если добросовестно написать периодический поток, вполне обычна ситуация, когда раньше него выполняются:
- другие потоки
- другие процессы
- внутренняя обработка ОС
- продукты безопасности
- вспомогательная обработка устройств
- фоновая синхронизация
3.2. DPC / ISR и драйверы
Этот момент довольно важен. Даже если привести в порядок приоритеты на стороне приложения, если DPC (Deferred Procedure Call) или ISR (Interrupt Service Routine) выполняются долго, всё это время user-mode-поток выполняться не может.
Частыми причинами становятся такие устройства и драйверы:
- USB
- Wi-Fi / Bluetooth
- хранилище
- аудио
- GPU
- ACPI / управление питанием
Даже если код приложения безупречен, его могут остановить обстоятельства, связанные с драйверами или оборудованием. Мысль «просто подниму приоритет приложения ещё выше и выиграю» здесь, как правило, приводит к разочарованию.
3.3. Page fault и память
Если в горячем пути возникает page fault (нужная страница отсутствует в памяти, и её приходится подгружать), задержка резко увеличивается.
Особенно стоит избегать таких паттернов:
- фиксация страницы при первом обращении
- отложенная загрузка
- подкачка страниц memory-mapped файлов
- динамическое выделение сверх необходимого
- большие объекты или фрагментированная куча
Для тела периодической обработки в самый раз выглядит подход: выделить нужную память заранее и один раз коснуться её при запуске.
3.4. Разрешение таймера и управление питанием
Подход «хочу выполнять действие каждую 1 мс, поэтому Sleep(1)» почти никогда не срабатывает.
Точность ожидания в Windows зависит от разрешения таймера, планирования и состояния питания.
Кроме того, нельзя упускать из виду, что настройка, повышающая разрешение таймера, немного улучшает точность ожидания, но одновременно даёт побочные эффекты на энергопотребление и поведение системы в целом.
3.5. Миграция между ядрами и нагрев
Когда поток мигрирует между ядрами, кешу приходится «прогреваться» заново. Зачастую ОС сама неплохо с этим справляется, но в условиях высокой нагрузки это становится источником колебаний.
Кроме того, при длительной работе нельзя игнорировать и нагрев. Когда включается термотроттлинг, ранее стабильный период может развалиться.
4. Практический чек-лист снижения задержек на обычной Windows
Здесь начинается практическая часть. Для причин, рассмотренных в предыдущем разделе, в виде чек-листа собрано, что проверять, чего избегать и что решить заранее на обычной Windows.
4.1. Периодический цикл и способ ожидания
Прежде всего, вот классический антипаттерн.
while (running)
{
Sleep(1);
Step();
}
Это не «период 1 мс», а цикл, который обычно ждёт чуть больше 1 мс, а сверху добавляет время выполнения Step().
Причём перерасход ожидания накапливается без изменений.
flowchart LR
subgraph Bad["На основе относительного времени"]
B1["Sleep(1)"] --> B2["Step()"]
B2 --> B1
end
B2 --> B3["Ошибка ожидания и время выполнения постепенно накапливаются"]
subgraph Good["На основе абсолютного срока"]
G1["next += period"] --> G2["WaitUntil(next - margin)"]
G2 --> G3["Короткий spin при необходимости"]
G3 --> G4["FastStep()"]
G4 --> G1
end
G4 --> G5["Меньше подвержен накоплению дрейфа"]
Чек-лист
- Периодический цикл не построен на основе
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.
flowchart LR
Input["Устройство / события получения"] --> Fast["fast path: получение, управление, минимальное копирование"]
Fast --> Queue["Очередь фиксированной длины"]
Queue --> Slow["slow path: сохранение, отправка, UI, агрегация"]
Fast --> Metrics["Запись lateness / miss / глубины очереди"]
Metrics --> Slow
То, что делается в fast path, стоит ограничить примерно этим списком.
- получение данных
- вычисление управляющих значений
- минимально необходимое копирование
- проставление временной метки
- постановка в очередь
- запись miss / overrun
Всё остальное сбрасывается в slow path.
Чек-лист
- В горячем пути нет записи в файл, сетевой отправки, записи в БД
- В горячем пути нет тяжёлого логирования,
Flush, синхронного RPC - fast path и slow path чётко разделены по потокам или зонам ответственности
- Очередь имеет фиксированную длину
- Политика на случай переполнения очереди определена заранее
- Ведётся наблюдение за числом miss, числом drop и глубиной очереди
- Обновление UI и агрегация логов вынесены в цикл с более низкой частотой
Когда очередь заполняется, безопаснее не оставлять политику расплывчатой.
flowchart TD
Overflow["Очередь заполнена"] --> Policy{"Что защищаем?"}
Policy -->|Важно последнее значение| Latest["Отбросить старые элементы, оставить последний"]
Policy -->|Важна каждая запись| All["Оповещение / остановка / управление потоком вверх"]
Policy -->|Для логирования| Log["Отбросить старые элементы, фиксировать только число drop"]
4.3. Приоритеты / MMCSS / background mode
Базовое правило работы с приоритетами — не поднимать всё подряд. На обычной Windows лучше работает подход «поднимать только важные потоки, а фоновую работу честно понижать». Background mode — это механизм, который склоняет к низкому приоритету не только CPU, но и такие ресурсы, как I/O.
flowchart TD
Work["Разделить работу"] --> Critical["Потоки, чувствительные к срокам"]
Work --> Worker["Сохранение / отправка / сжатие / агрегация"]
Work --> UI["UI"]
Critical --> P1["При необходимости повышенный приоритет или MMCSS"]
Worker --> P2["background mode / пониженный приоритет"]
UI --> P3["Обычный приоритет"]
P1 --> Warn["Не начинать сразу с REALTIME_PRIORITY_CLASS"]
Чек-лист
- Не все потоки установлены на высокий приоритет
- Повышены только потоки, действительно критичные по времени
- Фоновая работа — сохранение, отправка, сжатие, синхронизация — переведена в 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");
}
4.4. Память / GC / затраты первого запуска
Если в горячем пути на каждой итерации использовать new / malloc / List<T>.Add / конкатенацию строк / LINQ, рано или поздно на первый план выйдут особенности сборки мусора и перераспределения памяти. Сама по себе GC (сборка мусора) не является злом, но если писать код с большим количеством выделений, его влияние проявится как джиттер.
flowchart LR
Start["Запуск"] --> Alloc["Выделить нужные буферы"]
Alloc --> Touch["Один раз коснуться, чтобы прогреть страницы"]
Touch --> Warm["Завершить JIT / загрузку DLL / первый I/O заранее"]
Warm --> Measure["Затем — реальные измерения / реальная работа"]
Чек-лист
- В горячем пути нет выделения / освобождения памяти на каждой итерации
- Нужные буферы выделены заранее при запуске
- При запуске страницы один раз прогреты обращением
- Первый JIT, первая загрузка DLL, первый I/O не попадают в реальные измерения
- В цикле не растут огромные структуры или логи переменной длины
- Если
VirtualLockвсё же используется, то только для очень небольшой критической области
Проверки для .NET
- Для измерения времени используется
Stopwatch/Stopwatch.GetTimestamp() - В горячем пути нет LINQ, конкатенации строк,
ToString(), генерации больших логов async/awaitне заносится в горячий путь- Оценка до и после прогрева выполняется раздельно
4.5. Настройки питания / EcoQoS / разрешение таймера
Этот раздел выглядит неброско, но эффективен. Даже если довести код до идеала, результат не будет стабильным, если сверху сильно давит управление питанием.
flowchart TD
Power["Питание на обычной Windows"] --> AC["Работа от сети переменного тока"]
Power --> Mode["Режим питания: ближе к максимальной производительности"]
Power --> Plan["При необходимости — отдельный план питания для боевой среды"]
Power --> QoS["Держать чувствительные ко времени процессы подальше от EcoQoS"]
Power --> Timer["Проверить, как обрабатываются запросы на разрешение таймера"]
Чек-лист
- Боевая оценка сначала выполняется при питании от сети переменного тока
[Параметры] > [Система] > [Питание и батарея] > [Режим питания]установлен ближе к максимальной производительности- Во время работы не используется battery saver / режим с приоритетом энергосбережения
- Проверены фирменные утилиты производителя — их тихий / eco / battery-приоритетный режимы
- Чувствительные ко времени процессы неосторожно не помещены в EcoQoS (QoS с приоритетом энергосбережения)
IGNORE_TIMER_RESOLUTIONне включён на стороне чувствительного ко времени процесса- Проверено, не меняется ли действие запроса на разрешение таймера при сворачивании / скрытии окна
- Настройки питания для повседневного использования отделены от настроек для боевой среды / измерений / демонстраций
timeBeginPeriod полезен, если использовать его аккуратно, но это не панацея.
- вызывайте его непосредственно перед необходимостью
- по завершении возвращайте исходное значение через
timeEndPeriod - начиная с Windows 10 версии 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");
}
4.6. Размещение по CPU / миграция между ядрами / нагрев
В размещении по CPU часто лучше работает не резкая привязка к конкретным ядрам (hard affinity / CPU pinning), а начало с указания, близкого к soft affinity — «желательно работать преимущественно на этой группе ядер».
flowchart LR
Measure["Сначала измерить"] --> Ideal["SetThreadIdealProcessor / CPU Sets"]
Ideal --> Check{"Улучшение достаточное?"}
Check -->|Да| Keep["Остановиться на этом"]
Check -->|Нет| Hard["В последнюю очередь рассмотреть SetThreadAffinityMask"]
Measure --> Therm["Одновременно проверять температуру / частоту / долгую работу"]
Чек-лист
- Размещение по CPU меняется только после предварительного измерения
- Нет резкой привязки к конкретным ядрам с самого начала
- Сначала пробуются
SetThreadIdealProcessorили CPU Sets SetThreadAffinityMaskрассматривается как крайняя мера- При долгой работе проверяются температура, частота, термотроттлинг
- Проверены тихий режим и режим пониженного шума для портативных ПК
В качестве последовательности безопасен такой порядок.
- сначала измерить
- при необходимости — ideal processor / CPU Sets
- если улучшение всё ещё нужно — привязка к конкретным ядрам
Привязка к конкретным ядрам выглядит эффективной, но она сокращает пути отступления для ОС, поэтому при неосторожном использовании может, наоборот, сделать систему менее гибкой.
4.7. Разделение драйверов / DPC / ISR / внешних помех
Когда «иногда резко подскакивает только max» или «среднее хорошее, а p99.9 плохой», стоит заподозрить внешние помехи за пределами кода приложения.
flowchart TD
Spike["Возник всплеск late / miss / max"] --> Q1{"Собственное время обработки тоже велико?"}
Q1 -->|Да| App["Сократить горячий путь / уменьшить выделения / убрать I/O"]
Q1 -->|Нет| Q2{"Есть всплески DPC / ISR?"}
Q2 -->|Да| Driver["Проверить USB / Wi-Fi / Bluetooth / GPU / аудио / хранилище / ACPI / обновления драйверов"]
Q2 -->|Нет| Q3{"Есть page fault / GC / затраты первого запуска?"}
Q3 -->|Да| Mem["Предварительное выделение / прогрев / снижение нагрузки на кучу"]
Q3 -->|Нет| Q4{"Есть влияние батареи / энергосбережения / нагрева?"}
Q4 -->|Да| Pow["Питание от сети / настройки питания / охлаждение / длительные тесты"]
Q4 -->|Нет| ETW["Углубиться с помощью ETW / WPA / LatencyMon"]
Чек-лист
- Проверены драйверы вокруг Wi-Fi / Bluetooth / USB / хранилища / GPU / аудио
- Проведено сравнение с отключёнными ненужной облачной синхронизацией, индексированием, автообновлениями
- Проверено, ломается ли всё при сворачивании окна или выключении экрана
- Тенденции DPC / ISR просмотрены через LatencyMon или ETW
- Раздельно рассмотрено, «моя обработка тяжёлая» или «меня останавливают извне»
5. Измерение и оценка
5.1. Что записывать
Как минимум стоит фиксировать следующее.
- запланированное время цикла
- фактическое время начала
- фактическое время завершения
- lateness (насколько позже запланированного начала фактически стартовало)
- время выполнения
- число пропущенных сроков (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.
5.3. Чем измерять
Набор инструментов в целом стандартный.
- Измерение внутри приложения
Сначала собрать самостоятельно
period / lateness / execution time / queue depth / drop - ETW / WPR / WPA Углубиться в CPU, переключения контекста, DPC / ISR, page fault
- LatencyMon Прицелиться в колебания, вызванные драйверами
- Мониторинг температуры / частоты Следить за влиянием нагрева
flowchart LR
App["Измерение внутри приложения"] --> Dist["p50 / p95 / p99 / p99.9 / max"]
App --> Miss["miss / drop / глубина очереди"]
ETW["ETW / WPR / WPA"] --> Root["переключения контекста / DPC / ISR / page fault"]
Temp["Мониторинг температуры / частоты"] --> Root
Dist --> Decide["Определить приоритеты улучшений"]
Miss --> Decide
Root --> Decide
Дойти до WPA несколько трудозатратно, но это весьма эффективно для того, чтобы разделить, виноваты ли DPC / ISR или просто собственная обработка слишком тяжёлая.
5.4. Дисциплина тестирования
Одного тихого стендового окружения для тестирования недостаточно. Стоит как минимум раздельно рассмотреть такие условия.
- сразу после запуска, до прогрева
- после прогрева
- длительная непрерывная работа
- UI на переднем плане
- UI свёрнут / состояние, близкое к скрытому
- питание от сети переменного тока
- питание от батареи
- состояние с нагрузкой на сеть или диск
Оценка только на стенде легко приводит к тому, что проблемы, возникающие в реальной эксплуатации, остаются незамеченными. Поведение обычной Windows легко тянется за тем, «как её используют», поэтому важно проверять в условиях, приближенных к реальному использованию.
6. Краткое руководство по выбору
-
Класс 10–20 мс, редкие колебания можно поглотить → разделение fast / slow, очередь фиксированной длины, обычный — чуть повышенный приоритет, событийная модель часто уже достаточны
-
Класс 1–5 мс, нужно непрерывно укладываться → горячий путь без выделений памяти, выделенные потоки, MMCSS или аккуратная настройка приоритетов, высокоточный waitable timer, питание от сети переменного тока, настройки питания ближе к максимальной производительности
-
Приближение к менее чем 1 мс, и нельзя промахиваться даже при долгой работе под высокой нагрузкой → одним лишь user-mode на обычной Windows это весьма тяжело. Стоит рассмотреть перенос критичной части в другое место
-
GUI / логи / связь / БД сосуществуют все вместе → не тащить всё в схему «один процесс, один цикл», а разделять зоны ответственности, так как особенности последующих этапов легко ломают сроки предыдущих
7. Итог
Стоит зафиксировать две предпосылки.
- На обычной Windows цель — не гарантия hard real-time, а конфигурация soft real-time: небольшая задержка и джиттер, и отсутствие поломки при нарушении срока
- Самый большой эффект даёт не настройка приоритетов, а наведение порядка в горячем пути
На стороне реализации эффективны такие вещи.
- разделять fast path и slow path
- использовать очередь фиксированной длины и заранее решить политику на случай переполнения
- измерять через QPC, ждать через события / waitable timer
- в горячем пути избегать выделений памяти, блокирующего I/O, тяжёлых блокировок
На стороне эксплуатации эффективны такие вещи.
- работать при питании от сети переменного тока
- держать отдельные настройки питания для боевой среды
- сокращать ненужную фоновую нагрузку
- оценивать по p99 / p99.9 / max и числу пропусков
Мягкое реальное время на обычной Windows определяется не одними лишь настройками приоритета — если раздельно проработать проектирование, реализацию, настройки питания, измерения и эксплуатацию, можно получить весьма стабильную систему.
8. Источники
- Multimedia Class Scheduler Service
- AvSetMmThreadCharacteristicsW function
- SetThreadPriority function
- SetPriorityClass function
- timeBeginPeriod function
- CreateWaitableTimerExW function
- Acquiring high-resolution time stamps
- QueryPerformanceCounter function
- GetSystemTimePreciseAsFileTime function
- SetProcessInformation function
- VirtualLock function
- CPU Sets
- SetThreadIdealProcessor function
- SetThreadAffinityMask function
- Processor power management options
- Change the power mode for your Windows PC
- Power settings in Windows 11
- CPU Analysis (WPA / WPT)
Похожие статьи
Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.
Введение в ADR (Architecture Decision Record) — минимальный способ сохранить «почему мы спроектировали именно так» в небольшой команде
Код никогда не объясняет, почему он написан именно так. Разбираем, как использовать ADR (Architecture Decision Record) — одно решение, од...
Когда не стоит переводить Windows-приложение в веб: таблица решений и «разделение» как реальный выход
Запросов на перевод корпоративных Windows-приложений в веб становится всё больше, но для приложений с интеграцией оборудования, локальной...
Почему в Windows стоит предпочитать ожидание события, а не Sleep(1)
В Windows точность коротких timed wait зависит от гранулярности системных часов и планирования. Если вы ждёте появления работы, завершени...
Таблица решений: завершать работу приложения или продолжать при неожиданном исключении
Разбираем, когда после неожиданного исключения приложение стоит завершать, а когда можно продолжать работу — с точки зрения повреждения с...
Минимальный чек-лист безопасности при разработке Windows-приложений
Систематизируем в виде чек-листа базовые меры безопасности для бизнес-приложений на WPF / WinForms / WinUI / C++ / C#: права доступа, под...
Связанные темы
Эти страницы показывают тему статьи в более широком контексте услуг и решений.
Технические темы Windows
Раздел о разработке Windows, расследовании сбоев и использовании существующих активов.
Частые вопросы
Вопросы, которые часто возникают при консультациях по теме статьи.
- Можно ли выполнять обработку реального времени на 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 следует использовать только на время реальной необходимости, а затем возвращать исходное значение.
- Что сильнее всего помогает сократить задержку и джиттер?
- Не повышение приоритета, а то, чтобы сделать горячий путь коротким, фиксированной длины и неблокирующим. Fast path (получение / управление) и slow path (сохранение / связь / UI) разделяют и соединяют через очередь фиксированной длины, а в горячем пути избегают записи в файл, сетевой отправки, тяжёлого логирования и выделения памяти на каждой итерации. На стороне эксплуатации помогают питание от сети переменного тока, пересмотр режима питания, проверка EcoQoS и наведение порядка в фоновой нагрузке.
- По каким показателям оценивать стабильность периодической обработки?
- Не только по среднему значению, а по p99, p99.9, max и числу пропущенных сроков (missed deadline). Например, если среднее — 0,8 мс, а максимум — 28 мс, это означает, что обычно всё быстро, но иногда случаются крупные всплески, и на обычной Windows реальная проблема как раз проявляется в этом «хвосте» от p99 до max. Дополнительно стоит фиксировать всплески DPC/ISR, page fault, глубину очереди и колебания температуры/частоты, а оценку проводить раздельно по условиям: до и после прогрева, при длительной работе, в свёрнутом состоянии, при питании от батареи и т. д.
Об авторе
Страница с профилем автора статьи.
Го Комура
Представитель KomuraSoft LLC
Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.
Публичные ссылки