Планирование потоков
Динамическое повышение приоритета
Планировщик принимает решения на основе текущего приоритета потока, который может быть выше базового. Есть несколько ситуаций, когда имеет смысл повысить приоритет потока.
Например, после завершения операции ввода-вывода увеличивают приоритет потока, чтобы дать ему возможность быстрее начать выполнение и, может быть, вновь инициировать операцию ввода-вывода. Таким способом система поощряет интерактивные потоки и поддерживает занятость устройств ввода-вывода. Величина, на которую повышается приоритет, не документирована и зависит от устройства (рекомендованные значения для диска и CD - это 1, для сети - 2, клавиатуры и мыши - 6 и звуковой карты - 8). В дальнейшем в течение нескольких квантов времени приоритет плавно снижается до базового.
Другими примерами подобных ситуаций могут служить: пробуждение потока после состояния ожидания семафора или иного события; получение потоком доступа к оконному вводу.
Динамическое повышение приоритета решает также проблему голодания потоков, долго не получающих доступ к процессору. Обнаружив такие потоки, простаивающие в течение примерно 4 сек., система временно повышает их приоритет до 15 и дает им два кванта времени. Побочным следствием применения этой технологии может быть решение известной проблемы инверсии приоритетов [ Таненбаум ] . Эта проблема возникает, когда низкоприоритетный поток удерживает ресурс, блокируя высокоприоритетные потоки, претендующие на этот ресурс. Решение состоит в искусственном повышении его приоритета на некоторое время.
Динамическое повышение приоритетов призвано оптимизировать общую пропускную способность системы, однако от него выигрывают далеко не все приложения. Отключение динамического повышения приоритета можно осуществить при помощи функций SetProcessPriorityBoost и SetThreadPriorityBoost.
Прогон программы, демонстрация приоритетного планирования
#include <windows.h> #include <stdio.h> #include <math.h> void Calculations() { int i,N=50000000; double a,b; for ( i = 0; i<N; i++) { b=(double)i / (double)N; a=sin(b); } } DWORD WINAPI SecondThread( LPVOID lpParam ) { printf("Begin of Second Thread\n"); Calculations(); printf("End of Second Thread\n"); return 0; } VOID main( VOID ) { DWORD dwThreadId, dwThrdParam; HANDLE hThread; hThread = CreateThread( NULL, 0, SecondThread, &dwThrdParam, 0, &dwThreadId); if (hThread == NULL) { printf("CreateThread failed\n" ); return; } SetThreadPriority(hThread, THREAD_PRIORITY_ABOVE_NORMAL); SuspendThread(hThread); getchar(); ResumeThread(hThread); printf("Begin of First Thread\n"); Calculations(); printf("End of First Thread\n"); }
В приведенной программе два параллельных потока выполняют длительный счетный цикл (подпрограмма Calculations). Второй поток в силу более высокого приоритета выполняется раньше. Пара функций Suspend/ResumeThread (приостановка и возобновление потока) используется для фиксации начала соревнования. Если закомментировать SetThreadPriority, то можно будет увидеть, что оба потока заканчивают работу одновременно.
В качестве самостоятельного упражнения рекомендуется реализовать более гибкие сценарии планирования, например, с добавлением функций SwitchToThread (передача управления потоку), или Sleep (приостановка потока в течение заданного промежутка времени). В MSDN имеется описание множества полезных функций, связанных с планированием потоков.
Величина кванта времени
Величина кванта времени имеет критическое значение для эффективной работы системы в целом. Необходимо сохранить интерактивные качества системы и при этом избежать слишком частого переключения контекстов. Вероятно оптимальное значение кванта (доля секунды) должно обеспечивать обслуживание без переключения запроса пользователя, который занимает процессор ненадолго, после чего обычно генерирует запрос на ввод-вывод. В этом случае расходы на диспетчеризацию сводятся к минимуму и обеспечиваются приемлемые времена откликов.
По умолчанию начальная величина кванта в Windows Professional равна двум интервалам таймера, а в Windows Server эта величина увеличена до 12, чтобы свести к минимуму переключение контекста. Длительность интервала таймера определяется HAL и составляет примерно 10 мс для однопроцессорных x86 систем и 15 мс - для многопроцессорных. Величину интервала системного таймера можно определить с помощью свободно распространяемой утилиты Clockres (сайт http://sysinternals.com).
Выбор между короткими и длинными значениями можно сделать с помощью панели "свойства" "Моего компьютера". Величина кванта задается в параметре HKLM\SYSTEM\CurrentControlSet\Control\PriorityControl\Win32PrioritySeparation реестра.
Планирование в условиях многопроцессорности
Реентерабельность кода ядра позволяет ОС Windows поддерживать симметричные мультипроцессорные системы (процессоры идентичны). Необходимость загрузки нескольких процессоров усложняет задачу планирования. Количество процессоров система определяет при загрузке, и эта информация становится доступной приложениям через функцию GetSystemInfo. Число процессоров, используемых системой, может быть ограничено с помощью параметра NumPcs из файла Boot.ini.
Ведение отдельных очередей готовых к выполнению потоков для каждого из процессоров может иметь следствием неравномерную загрузку процессоров, поэтому используется общая очередь потоков в состоянии готовности. Любой поток становится в очередь и планируется на любой доступный процессор. Поскольку в системе нет главного процессора, каждый процессор занимается самопланированием и выбирает поток из очереди готовности. Чтобы гарантировать, что два процессора не выберут один и тот же поток, для каждого процессора организовывается эксклюзивный доступ к данной очереди за счет использования спин-блокировки диспетчера ядра.
Привязка к процессорам
У каждого потока имеется маска привязки к процессорам (affinity mask), указывающая, на каких процессорах можно выполнять данный поток. По умолчанию Windows использует нежесткую привязку (soft affmity) потоков к процессорам. Это означает, что некоторое преимущество имеет последний процессор, на котором выполнялся поток, чтобы повторно использовать данные из кэша этого процессора (родственное планирование). Потоки наследуют маску привязки процесса. Изменение привязки процесса и потока может быть осуществлено с помощью Win32-функций SetProcessAffinityMask и SetThreadAfftnityMask или с помощью инструментальных средств Windows (например, это может сделать диспетчер задач). Есть также возможность сформировать априорную маску привязки в файле образе запускаемого процесса.
Помимо номера последнего процессора в блоке ядра потока KTHREAD хранится номер идеального процессора (ideal processor) - предпочтительного для выполнения данного потока. Идеальный процессор выбирается случайным образом при создании потока. Это значение увеличивается на 1 всякий раз, когда создается новый поток, поэтому создаваемые потоки равномерно распределяются по набору доступных процессоров. Поток может поменять это значение с помощью функции SetThreadIdealProcessor.
Готовый к выполнению поток система пытается подключить к простаивающему процессору. Если таких несколько, то предпочтение отдается идеальному процессору данного потока, а затем последнему из процессоров, на котором поток выполнялся. Если все процессоры заняты, то делается проверка на возможность вытеснить какой-либо выполняющийся или ждущий поток (в первую очередь на идеальном процессоре, затем - на последнем для данного потока). Если вытеснение невозможно, новый поток помещается в очередь готовых потоков с соответствующим уровнем приоритета и ждет выделения процессорного времени.
Таким образом, в ОС Windows реализовано двухуровневое планирование. На верхнем уровне алгоритма потоки приписываются конкретным (идеальным, последним, наименее загруженным) центральным процессорам, в результате чего у каждого процессора создается своя очередь потоков. На нижнем уровне каждым процессором осуществляется реальное планирование при помощи приоритетов и других средств.
Например, если какой-либо процессор начинает простаивать, у загруженного работой процессора отбирается поток и отдается ему. Двухуровневое планирование равномерно распределяет нагрузку среди процессоров и использует преимущество родственности кэша.
Жесткая привязка (hard affinity), выполняемая с помощью функций SetProcessAffinityMask и SetThreadAfftnityMask, целесообразна в архитектурах с неунифицируемым (NUMA) доступом, где скорость доступа к памяти зависит от взаимного расположения процессоров и банков памяти на системных платах.
Заключение
Процессорное время - ограниченный ресурс, поэтому планирование - важная и критичная для производительности операция. Один из ключевых вопросов - выбор момента для запуска процедуры планирования. В системе реализовано приоритетное вытесняющее планирование с динамическими приоритетами. Для удобства пользователя и мобильности программ поддерживается слой абстрагирования приоритетов. Механизмы привязки позволяют организовать эффективное исполнение программ в многопроцессорных системах.