Круговое планирование, известное как Round Robin, выглядит обманчиво просто: каждому готовому потоку достаётся отрезок процессорного времени, отрезок истёк, поток отходит в конец очереди, следующему дают поиграть дальше. В чистом виде так работает учебный пример. В реальном ядре Windows круговая дисциплина живёт внутри каждой приоритетной очереди, поверх неё навешаны вытеснение, повышения приоритета, выбор ядра в многоядерной машине и, с недавних пор, различение быстрых и экономичных ядер. Системный программист, который хочет понимать, почему программа тормозит именно так, а не иначе, обязан видеть всю эту механику целиком.
Квант времени и кто решает сколько он длится
Квант в Windows называется quantum и измеряется не в миллисекундах напрямую, а в условных единицах, из расчёта три единицы за один тик системного таймера. На клиентских редакциях типичный квант составляет два тика, что при стандартном тике даёт примерно 20–40 мс, а при уменьшенном разрешении таймера вилка растягивается до 20–60 мс. На серверных редакциях цикл длиннее: там по умолчанию квант фиксированный и заметно больше, около шести тиков, потому что фоновым службам важнее пропускная способность, чем отзывчивость окна под курсором.
Эта же развилка спрятана в панели производительности, где предлагается оптимизировать систему для программ или для фоновых служб. Выбор "программы" даёт короткий переменный квант и умножение кванта процесса переднего плана, выбор "фоновые службы" даёт длинный фиксированный квант всем подряд. Различие принципиально: короткий квант чаще переключает контекст и тратит больше тактов на саму коммутацию, длинный квант реже прерывает вычислительные потоки и лучше кормит пакетную нагрузку.
Внутри кванта ядро ведёт счётчик. Каждый тик таймера из кванта текущего потока вычитается тройка, и когда счёт обнуляется, срабатывает вытеснение по истечении кванта. Потоку при этом никто не ломает исполнение в произвольной точке ради прихоти: прерывание таймера приходит само, обработчик смотрит очередь готовых и решает, остаться на месте или пересесть.
Очереди готовых потоков и тридцать два уровня приоритета
Планировщик Windows держит тридцать две очереди готовых потоков, по одному на уровень приоритета от 0 до 31. Уровни 16–31 считаются классом реального времени, уровни 1–15 динамическим диапазоном, ноль зарезервирован под поток обнуления страниц. Внутри одной очереди царит чистый Round Robin: потоки равного приоритета обходят процессор по кругу, каждый получает свой квант.
Вытеснение случается по двум поводам. Первый уже упомянут: квант кончился, поток уходит в хвост своей очереди. Второй жёстче: в систему проснулся поток с более высоким приоритетом, и текущий снимается с ядра немедленно, даже не доиграв квант. Остаток кванта при этом сгорает, при следующем запуске начинается новый отсчёт. Именно поэтому короткая вспышка активности высокоприоритетного потока способна вытеснять соседей снова и снова. Отдельно стоит помнить про добровольное переключение: поток, который вошёл в ожидание объекта ядра, уступает ядро до конца кванта, и это самый частый тип смены контекста в честной системе. Принудительное вытеснение против воли потока, наоборот, должно быть редким событием.
Выбор следующего потока детерминирован: сканируются очереди от 31 вниз, первая непустая очередь даёт головного потока. Никакой лотереи. Приоритет, который видит прикладной код через классы процесса и относительные уровни потока, превращается в конкретное число 0–31 по таблице внутри ядра, и дальше судьбу решает только это число плюс временные надбавки.
Affinity наборы процессоров и нечестность чистого круга
Маска affinity привязывает поток к подмножеству логических процессоров. Это инструмент точной настройки: убрать шумный поток с ядра, где живёт обработчик прерываний сетевого адаптера, или удержать кэш горячим. На больших машинах с более чем шестьюдесятью процессорами вступают в игру группы процессоров, и поток по умолчанию живёт в одной группе, пока явно не распределён. На системах с большим числом ядер Windows также применяет наборы процессоров, которые ограничивают, какие ядра доступны данному процессу, не давая диспетчеру раскидать потоки по всей топологии.
Теперь о главной претензии к чистому Round Robin. Круговая очередь честна к тем, кто готов сжечь квант целиком, то есть к вычислительным потокам. Поток, связанный вводом-выводом, ведёт себя иначе: он просыпается, успевает обменяться с драйвером несколькими сотнями инструкций и снова уходит спать, недобрав кванта. В чистой круговой схеме он каждый раз встаёт в конец очереди и ждёт полный оборот, хотя просил всего ничего. Число смен контекста на него тратится такое же, а реальной работы он выдаёт меньше. Интерактивная программа, клавиатурный ввод, сетевой отклик - всё это I/O-bound паттерны, и чистый круг их систематически штрафует.
Windows отвечает надбавками приоритета за завершение ожидания. Поток, вышедший из ожидания на диске, сети, клавиатуре, получает временное повышение, поток процесса переднего плана получает удлинённый квант, поток, проголодавшийся несколько секунд, раз в какое-то время подхватывается механизмом balance set manager и прогоняется вне очереди с удвоенным квантом. Надбавка тает постепенно: после каждого кванта приоритет опускается на уровень, пока не вернётся к базовому. Это превращает круговую дисциплину в гибрид, где I/O-bound потоки получают компенсацию за свою краткосрочность, а интерактивность выигрывает у честной очереди. Стоит добавить, что надбавки выдаются не всем ожиданиям подряд и не навечно: ожидание на объекте синхронизации, удерживаемом другим вычислительным потоком, поощряется слабее, чем ответ драйвера диска, а повышение никогда не выводит поток в диапазон реального времени. Такой потолок защищает систему от случайного захвата процессора обычной программой, которая просто часто делает ввод-вывод.
Ещё одна тонкость касается самого расходования кванта. Поток, которому на вход пришло событие, может успеть обработать его за долю кванта и снова заснуть. С точки зрения планировщика это готовый поток, который регулярно недоедал свою порцию. Именно из наблюдения за такой асимметрией и родились механизмы компенсации: честный круг без коррекций давал бы ровно обратный желаемому результат, при котором полезная короткая работа обслуживалась медленнее бесполезной длинной.
Голодание инверсия приоритетов и урок одного зонда
Система с жёстким вытеснением порождает два классических дефекта. Голодание наступает, когда потоки с высоким приоритетом приходят настолько часто, что низкоприоритетный вообще не получает ядра. Инверсия приоритетов коварнее: высокоприоритетный поток ждёт объект синхронизации, который держит низкоприоритетный, а тот в свою очередь вытесняется средними. Получается, что высокий фактически едет со скоростью средних, которых он должен был бы обгонять.
Это не абстракция из учебника. В 1997 году аппарат Mars Pathfinder на поверхности Марса начал уходить в перезагрузки. Причина оказалась именно в инверсии: высокоприоритетная задача управления шиной ждала мьютекс, который держала низкоприоритетная задача метеоданных, а среднеприоритетная коммуникационная задача с длинным квантом не давала низкой доработать. Сторожевой таймер видел просрочку и перезапускал систему. Лечение пришлось заливать по радиоканалу на другую планету: включили наследование приоритетов, при котором владелец объекта временно поднимается до приоритета самого важного ожидающего. В ядре Windows аналогичную роль играет механизм, который прокидывает приоритет ожидающего потока владельцу объекта на время владения.
Вывод практический: любой поток, берущий общий объект синхронизации, обязан отпускать его быстро, а приоритеты участников обмена стоит держать рядом. Длинные критические секции в низкоприоритетном коде - это минное поле, которое срабатывает под нагрузкой, а не на тестовом стенде.
Многоядерность выбор ядра и гетерогенное планирование
Когда ядер много, вопрос "кого запустить" дополняется вопросом "где запустить". У каждого потока Windows хранит идеальный процессор и последний использованный. Диспетчер сначала смотрит на идеальное ядро, затем на предыдущее, затем на любое свободное из маски affinity. Логика кэшевой: вернуться туда, где часть данных ещё тёплая в кэше ядра, дешевле, чем грузиться на холодное ядро. Простаивающие ядра при этом не совсем простаивают: поток простоя каждого ядра подхватывает работу, а механизм парковки ядер способен усыплять лишние ядра на лёгкой нагрузке, экономя энергию, и распарковывать их при росте очередей.
С выходом гибридных архитектур, где рядом стоят производительные ядра и экономичные ядра в духе big.LITTLE, а у Intel начиная с двенадцатого поколения это P-cores и E-cores, задача усложнилась. Разные ядра имеют разную скорость и разный аппетит по энергии, и простого числа приоритета уже мало. В кристалл встроен блок Thread Director, который наблюдает за характером исполняемого кода и подсказывает операционной системе, какой класс ядра этому потоку подходит: фоновая скучная работа едет на экономичные, активная задержко-чувствительная на производительные. Windows 11 умеет принимать эти подсказки и размещать потоки гетерогенно, а также классифицировать качество обслуживания потоков, чтобы фоновая задача не лезла на быстрое ядро без нужды. Для системного программиста это значит, что affinity, зашитая по старой памяти, способна поломать всю эту логику и прибить тяжёлый поток к слабому ядру.
Измерение и физика времени реакции
Теория планирования проверяется счётчиками. Первый инструмент - Performance Monitor и его счётчик Context Switches per second из объекта System: это число переключений контекста в секунду по всей машине. Здоровое значение зависит от нагрузки, но резкий рост без роста полезной работы намекает, что процессор жуёт сам себя. Объект Processor Queue Length показывает, сколько потоков стоит в очереди готовых; устойчивое значение выше двух на ядро говорит о конкуренции за процессор. Счётчики по отдельным потокам в объекте Thread дают приоритет, состояние и число переключений каждого потока, а xperf и WPA позволяют увидеть сам график: кто кого вытеснил, сколько просидел в очереди, где потерян квант.
Вторая половина истории - таймер. По умолчанию период системного таймера на клиенте около 15.6 мс, и тики приходят с этим шагом. Программы могут попросить точнее через API разрешения таймера, и с современными редакциями это разрешение стало по-настоящему процессным: один процесс, попросивший 0.5 мс, больше не крутит таймер всей машины. Гранулярность 0.5 мс важна для мультимедиа, игровых циклов и точных задержек: Sleep и ожидания объектов округляются к разрешению таймера, и на грубом разрешении просьба подождать миллисеку превращается в пятнадцать. Расплата - энергия: частые тики мешают ядрам проваливаться в глубокие состояния сна.
Методичная проверка планировочной гипотезы выглядит так:
- Снять базовую линию: Context Switches per second, Processor Queue Length, загрузку ядер.
- Поднять нагрузку и переснять те же счётчики, сопоставив рост переключений с ростом полезной работы.
- Через WPA найти потоки с наибольшим временем ожидания в очереди готовых.
- Проверить ручные affinity и приоритеты в коде на предмет конфликта с гибридной топологией.
- Проверить вызовы изменения разрешения таймера и их удержание на время цикла, а не навсегда.
Понимание Round Robin внутри многоуровневого планировщика превращает жалобу "иногда тормозит" в конкретную цепочку: квант, очередь, вытеснение, надбавка, ядро. Каждое звено измеримо, и каждое поддаётся осмысленной настройке.