|
|
|||
|
wm-help.net -> Электронная библиотека -> C++/C#/C -> Джеффри РИХТЕР "Windows для профессионалов" -> Глава 7. Планирование потоков, приоритет и привязка к процессорамГлава 7. Планирование потоков, приоритет и привязка к процессорамГЛАВА 7 Планирование потоков, приоритет и привязка к процессорам Операционная система с вытесняющей многозадачностью должна использовать тот или иной алгоритм, позволяющий ей распределять процессорное время между пото ками Здесь мы рассмотрим алгоритмы, применяемые в Windows 98 и Windows 2000. Б главе 6 мы уже обсудили структуру CONTEXT, поддерживаемую в объекте ядра "поток", и выяснили, что она отражает состояние регистров процессора на момент последнего выполнения потока процессором Каждые 20 мс (или около того) Windows просматривает все существующие объекты ядра "поток" и отмечает те из них, кото рые могут получать процессорное время. Далее она выбирает один из таких объек тов и загружает в регистры процессора значения из его контекста Эта операция на зывается переключением контекста (context switching) По каждому потоку Windows ведет учет того, сколько раз он подключался к процессору. Этот показатель сообща ют специальные утилиты вроде Microsoft Spy++ Например, на иллюстрации ниже показан список свойов одного из потоков. Обратите внимание, что этот поток под ключался к процессору 37379 раз
Поток выполняет код и манипулирует данными в адресном пространстве своего процесса Примерно через 20 мс Windows сохранит значения регистров процессора в контексте потока и приостановит сго выполнение. Далее система просмотрит ос тальные объекты ядра "поток", подлежащие выполнению, выберет один из них, заг рузит его контскст в регистры процессора, и все повторится Этот цикл операций — выбор потока, загрузка его контекста, выполнение и сохранение контекста — начи нается с момента запуска системы и продолжается до cc выключения. Таков вкратце механизм планирования работы множества потоков. Детали мы обсудим позже, но главное я уже показал Все очень просто, да? Windows потому и называется системой с вытесняющей многозадачностью, что в любой момент может приостановить любой поток и вместо него запустить другой. Как Вы еще увидите, этим механизмом можно управлять, правда, крайне ограниченно. Всегда помните: Вы не в состоянии гарантировать, что Ваш поток будет выполняться непрерывно, что ника кой другой поток не получи'1 доступ к процессору и т д.
Хочу особо подчеркнуть, что система планирует выполнение только тех потоков, которые могут получать процессорное время, но большинство потоков в системе к таковым не относится. Так, у некоторых объектов-потоков значение счетчика просто ев (suspend count) больше 0, а значит, соответствующие потоки приостановлены и не получают процессорное время. Вы можете создать приостановленный поток вызовом CreateProcess или CreateThread с флагом CREATESUSPENDED (В следующем разделе я расскажу и о таких функциях, как SuspendThread и ResumeThread.) Кроме приостановленных, существуют и другие потоки, не участвующие в распре делении процессорного времени, — они ожидают каких-либо событий. Например, если Вы запускаете Notepad и не работаете в нем с текстом, его поток бездействует, а система не выделяет процессорное время тем, кому нечего делать. Но стоит лишь сместить его окно, прокрутить в нем текст или что-то ввести, как система автомати чески включит поток Notepad в число планируемых Это вовсе не означает, что по ток Notepad тут жс начнет выполняться. Просто система учтет его при планировании потоков и когда-нибудь выделит ему время — по возможности в ближайшем будущем Приостановка и возобновление потоковВ объекте ядра "поток" имеется переменная — счетчик числа простоев данного по тока При вызове CreateProcess или CreateThread он инициализируется значением, рав ным 1, которое запрещает системе выделять новому потоку процессорное время. Та кая схема весьма разумна: сразу после создания поток не готов к выполнению, ему нужно время для инициализации. После гого как поток полностью инициализирован, CreateProcess или CreateThread проверяет, не передан ли ей флаг CREATE_SUSPENDED, и, если да, возвращает управ ление, оставив поток в приостановленном состоянии В ином случае счетчик простоев обнуляется, и поток включается в число планируемых — если только он не ждет ка кого-то события (например, ввода с клавиатуры). Создав поток в приостановленном состоянии, Выможете настроить некоторые его свойства (например, приоритет, о котором мы поговорим позже). Закончив настройку, Вы должны разрешить выполнение потока. Для этого вызовите ResumeThread и пере дайте описатель потока, возвращенный функцией CreateThread (описатель можно взять и из структуры, на которую указывает параметр ppiProcInfo, передаваемый в CreateProcess).
Если вызов ResumeThread прошел успешно, она возвращает предыдущее значение счетчика простоев данного потока; в ином случае — 0xFFFFFFFF. Выполнение отдельного потока можно приостанавливать несколько раз. Если поток приостановлен 3 раза, то и возобновлен он должен быть тоже 3 раза — лишь тогда система выделит ему процессорное время. Выполнение потока можно приос тановить не только при его создании с флагом CREATE_SUSPENDED, но и вызовом SuspendThread. DWORD RuspendThread(HANDLE hThread); Любой поток может вызвать эту функцию и приостановить выполнение другого потока (конечно, если его описатель известен). Хоть об этом нигде и не говорится (но я все равно скажу!), приостановить свое выполнение поток способен сам, а во зобновить себя без посторонней помощи — нет. Как и ResumeThread, функция Sus pendThread возвращает предыдущее значение счетчика простоев данного потока. Поток можно приостанавливать не более чем MAXIMUM_SUSPEND_COUNT раз (в файле WinNT.h это значение определено как 127). Обратите внимание, что Suspend Thread в режиме ядра работает асинхронно, но в пользовательском режиме не выпол няется, пока потокостается в приостановленном состоянии. Создавая реальное приложение, будьте осторожны с вызовами SuspendThread, так как нельзя заранее сказать, чем будет заниматься его поток в момент приостановки. Например, он пытается выделить память из кучи и поэтому заблокировал к ней дос туп. Тогда другим потокам, которым тоже нужна динамическая память, придется ждать его возобновления. SuspendThread безопасна только в том случае, когда Вы точно знаете, что делает (или может делать) поток, и предусматриваете все меры для исклю чения вероятных проблем и взаимной блокировки потоков. (О взаимной блокировке и других проблемах синхронизации потоков я расскажу в главах 8, 9 и 10.) Приостановка и возобновление процессовВ Windows понятия "приостановка" и "возобновление" неприменимы к процессам, так как они не участвуют в распределении процессорного времени. Однако меня не рая спрашивали, как одним махом приостановить все потоки определенного процесса. Это можно сделать из другого процесса, причем он должен быть отладчиком и, в ча стности, вызывать функции вроде WaitForDebugEvent и ContinueDebugEvent. Других способов приостановки всех потоков процесса в Windows нет: програм ма, выполняющая такую операцию, может "потерять" новые потоки. Система должна как-то приостанавливать в этот период не только все существующие, но и вновь со здаваемые потоки. Microsoft предпочла встроить эту функциональность в системный механизм отладки. Вам, конечно, не удастся написать идеальную функцию SuspendProcess, но вполне по силам добиться ec удовлетворительной работы во многих ситуациях. Вот мой ва риант функции SuspendProcess.
Для перечисления списка потоков я использую ToolHelp функции (они рассмат ривались в главе 4). Определив потоки нужною процесса, я вызываю OpenThread.
Это новая функция, которая появилась в Windows 2000 Она находит объект ядра "поток" по идентификатору, указанному в dwTbreadJD, увеличивает его счетчик поль зователей на 1 и возвращает описатель объекта Получив описатель, я могу передать его в SuspendThread (или ResumeThread) OpenThread имеется только в Windows 2000, поэтому моя функция SuspendProcess не будет работать ни в Windows 95/98, ни в Windows NT 4 0 Вероятно, Вы уже догадались, почему SuspendProcess будет срабатывать не во всех случаях: при перечислении могут создавайся новые и уничтожаться существующие потоки. После вызова CreateToolhelp32Snapshot в процессе может появиться новый поток, который моя функция уже не увидит, а значит, и не приостановит Впослед ствии, когда я попытаюсь возобновить потоки, вновь вызвав SuspendProcess, она во зобновит поток, который собственно и не приостанавливался. Но может быть еще хуже- при перечислении текущий поток уничтожается и создастся новый с тем же идентификатором. Тогда моя функция приостановит неизвестно какой поток (и даже непонятно в каком процессе). Конечно, все эти ситуации крайне маловероятны, и, если Вы точно представляе те, что делает интересующий Вас процесс, никаких проблем не будет. В общем, ис пользуйте мою функцию на свой страх и риск. Функция SleepПоток может сообщить системе не выделять ему процессорное время на определен ный период, вызвав:
Эта функция приостанавливает поток па dwMilliseconds миллисекунд. Отметим несколько важных моментов, связанных с функцией Sleep.
Переключение потоковФункция SwitchToThread позволяет подключить к процессору другой поток (если он есть):
Когда Вы вызываете эту функцию, система проверяет, есть ли поток, которому не хватает процессорного времени Если нет, SwitchToThread немедленно возвращает управление, а если да, планировщик отдает ему дополнительный квант времени (при оритет этого потока может быть ниже, чем у вызывающего). По истечении , этого кван та планировщик возвращается в обычный режим работы SwitchToThread позволяет потоку, которому не хватает процессорного времени, отнять этот ресурс у потока с более низким приоритетом. Она возвращает FALSE, если на момент ее вызова в системе нет ни одного потока, готового к исполнению, в ином случае — ненулевое значение. Вызов SwitchToThread аналогичен вызову Sleep с передачей в dwMilliseconds нуле вого значения. Разница лишь в том, что SwitchToThread дает возможность выполнять потоки с более низким приоритетом, которым не хвачает процессорного времени, а Sleep действует без оглядки на "голодающие" потоки.
Определение периодов выполнения потокаИногда нужно знать, сколько времени затрачивает поток на выполнениетой или иной операции Многие в таких случаях пишут что-то вроде этого:
Этот код основан на простом допущении, что он нс будет прерван. Но в операци онной системе с вытесняющей многозадачностью никто не знает, когда поток полу чит процессорное время, и результат будет сильно искажен. Что нам здесь нужно, так это функция, которая сообщает время, затраченное процессором на обработку дан ного потока. К счастью, в Windows есть такая функция:
GetThreadTimes возвращает четыре временных параметра:
С помощью этой функции можно определить время, необходимое для выполне ния сложного алгоритма:
Заметим, что существует еще одна функция, аналогичная GetThreadTimes и при менимая ко всем потокам в процессе:
GetProcessTimes возвращает временные параметры, суммированные по всем пото кам (даже уже завершенным) в указанном процессе Так, время выполнения ядра бу дет суммой периодов времени, затраченного всеми потоками процесса на выполне ние кода операционной системы.
GetThreadTimes не годится для высокоточного измерения временных интервалов — для этого в Windows предусмотрено двe специальные функции:
Они построены на том допущении, что поток не вытесняется, поскольку высоко точные измерения проводятся, как правило, в очень быстро выполняемых блоках кода. Чтобы слегка упростить работу с этими функциями, я создал следующий С++ - класс:
Я применяю этот класс так:
Структура CONTEXTК этому моменту Вы должны понимать, какую важную роль играет структура CONTEXT в планировании потоков. Система сохраняет в ней состояние потока перед самым отключением его от процессора, благодаря чему его выполнение возобновляется с того места, где было прервано Вы, наверное, удивитесь, но в документации Platform SDK структуре CONTEXT отведен буквально один абзац: "В структуре CONTEXT хранятся данные о состоянии регистров с учетом специ фики конкретного процессора. Она используется системой для выполнения различ ных внутренних операций. В настоящее время такие структуры определены для про цессоров Intel, MIPS, Alpha и PowerPC. Соответствующие определения см. в заголовоч ном файле WinNT.h" В документации нет ни слова об элементах этой структуры, набор которых зави сит от типа процессора. Фактически CONTEXT — единственная из всех структур Windows, специфичнаядля конкретного процессора. Так из чего же состоит структура CONTEXT Давайте посмотрим Ее элементы чет ко соответствуют регистрам процессора. Например, для процессоров x86 в число элементов входят Eax, Ebx, Ecx, Edx и т д., а для процессоров Alpha — IntVO, IntTO, IntT1, IntSO, IntRa, IntZero и др. Структура CONTEXT для процессоров x86 выглядит так.
Эта структура разбита на несколько разделов. Раздел CONTEXT_CONTROL содер жит управляющие регистры процессора: указатель команд, указатель стека, флаги и адрес возврата функции. (В отличис от x86, который при вызове функции помещает адрес возврата в стек, процессор Alpha сохраняет адрес возврата в одном из регист ров,) Раздел CONTEXT_INTEGER соответствует целочисленным регистрам процессо ра, CONTEXT_FLOATING_POINT — регистрам с плавающей точкой, CONTEXT_SEG MENTS — сегментным регистрам (только для x86), CONTEXT_DEBUG_REGISTERS — регистрам, предназначенным для отладки (только для x86), a CONTEXT_EXTEN DED_REGISTERS — дополнительным регистрам (только для x86). Windows фактически позволяет заглянуть внутрь объекта ядра "поток" и получить сведения о текущем состоянии регистров процессора. Для этого предназначена функция:
Создайте экземпляр структуры CONTEXT, инициализируйте нужные флаги (в эле менте ContextFlags) и передайте функции GetThreadContext адрес этой структуры. Функция поместит значения в элементы, сведения о которых Вы запросили Прежде чем обращаться к GetThreadContext, приостяновите поток вызовом Sus pendThread, иначе поток может быть подключен к процессору, и значения регистров существенно изменятся. На самом деле у потока есть два контекста- пользовательско го режима и режима ядра. GetThreadContext возвращает лишь первый из них. Если Вы вызываете SuspendThread, когда поток выполняет код операционной системы, пользо вательский контекст можно считать достоверным, даже несмотря на то что поток еще не остановлен (он всс равно не выполнит ни одной команды пользовательского кода до последующего возобновления) Единственный элемент структуры CONTEXT, которому не соответствует какой либо регистр процессора, — ContextFlags. Присутствуя во всех вариантах этой струк туры независимо от типа процессора, он подсказывает функции GetThreadContext, значения каких регистров Вы хотите узyать. Например, чтобы получить значения управляющих регистров для потока, напишите что-то вроде:
Перед вызовом GetThreadContext надо инициализировать элемент ContextFlags. Чтобы получить значения как управляющих, так и целочисленных регистров, иници ализируйте его так
Есть еще один идентификатор, позволяющий узнать значения важнейших регис тров (т. e. используемых, по мнению Microsoft, чаще всего):
CONTEXT_FULL определен в файле WinNT.h, как показано в таблице.
После возврата из GetThreadContext Вы легко проверите значения любых регист ров для потока, но помните, что такой код зависит от типа процессора В следующей таблице перечислены элементы структуры CONTEXT, соответствующие указателям команд и стека для разных типов процессоров
Даже удивительно, какой мощный инструмент дает Windows в руки разработчи ка! Но есть вещь, от которой Вы придете в полный восторг- значения элементов CONTEXT можно изменять и передавать объекту ядра "поток" с помощью функции SetThreadContext.
Перед этой операцией поток тожe нужно приостановить, иначе результаты могут быть непредсказуемыми. Прежде чем обращаться к SetThreadContext, инициализируйте элемент ContextFlags, как показано ниже.
Этот код, вероятно, приведет к ошибке защиты (нарушению доступа) в удаленном потоке; система сообщит о необработанном исключении, и удаленный процесс бу дет закрыт. Все верно — нс Ваш, а удаленный. Вы благополучно обрушили другой процесс, оставив свой в целости и сохранности! Функции GetTbreadContexf и SetThreadContext наделяют Вас огромной властью над потоками, но пользоваться ею нужно с осторожностью. Вызывают их лишь считан ные приложения. Эти функции предназначены для отладчиков и других инструмен тальных средств, хотя обращаться к ним можно из любых программ Подробнее о структуре CONTEXT мы поговорим в главе 24. Приоритеты потоковВ начале главы я сказал, что поток получает доступ к процессору на 20 мс, после чего планировщик переключает процессор на выполнение другого потока. Так происхо дит, только если у всех потоков один приоритет, но на самом деле в системе суще ствуют потоки с разными приоритетами, а это меняет порядок распределения про цессорного времени. Каждому потоку присваивается уровень приоритета — от 0 (самый низкий) до 31 (самый высокий). Решая, какому потоку выделить процессорное премя, система сна чала рассматривает только потоки с приоритетом 31 и подключает их к процессору по принципу карусели. Если поток с приоритетом 31 нс исключен из планирования, он немедленно получает квант времени, по истечении которого система проверяет, есть ли еще один такой поток. Если да, он тоже получает свой квант процессорного времени. Пока в системе имеются планируемые потоки с приоритетом 31, ни один поток с более низким приоритетом процессорного времени не получает. Такая ситуация на зывается "голоданием* (starvation). Она наблюдается, когда потоки с более высоким приоритетом так интенсивно используют процессорное время, что остальные прак тически не работают. Вероятность этой ситуации намного ниже в многопроцессор ных системах, где потоки с приоритетами 31 и 30 могут выполняться одновременно. Система всегда старается, чтобы процессоры были загружены работой, и они проста ивают только в отсутствие планируемых потоков. На первый взгляд, в системе, организованной таким образом, у потоков с низким приоритетом нет ни единого шанса на исполнение. Но, как я уже говорил, зачастую потоки как раз и не нужно выполнять. Например, если первичный поток Вашего про цесса вызывает GetMessage, а система видит, что никаких сообщений пока нет, она приостанавливает его выполнение, отнимает остаток неиспользованного времени и тут же подключает к процессору другой ожидающий поток. И пока в системе не по явятся сообщения для потока Вашего процесса, он будет простаивать — система не станет тратить на него процессорное время. Но вот в очереди этого потока появля ется сообщение, и система сразу же подключает его к процессору (если только в этот момент не выполняется поток с более высоким приоритетом). А теперь обратите внимание на еще один момент. Потоки с более высоким при оритетом всегда вытесняют потоки с более низким приоритетом независимо от того, исполняются последние или нет. Допустим, процессор исполняет поток с приорите том 5, и тут система обнаруживает, что поток с более высоким приоритетом готов к выполнению. Что будет? Система остановит поток с более низким приоритетом — даже ссли не истек отведенный ему квант процессорного времени — и подключит к процессору поток с более высоким приоритетом (и, между прочим, выдаст ему пол ный квант времени), Кстати, при загрузке системы создается особый поток — поток обнуления стра ниц (zero page thread), которому присваивается нулевой уровень приоритета. Ни один поток, кроме этого, не может иметь нулевой уровень приоритета Он обнуляет сво бодные страницы в оперативной памяти при отсутствии других потоков, требующих внимания со стороны системы. Абстрагирование приоритетовСоздавая планировщик потоков, разработчики из Microsoft прекрасно понимали, что он не подойдет на все случаи жизни. Они также осознавали, что со временем "назна чение" компьютера может измениться Например, в момент выпуска Windows NT со здание приложений с поддержкой OLE еще только начиналось. Теперь такие прило жения — обычное дело. Кроме того, значительно расширилось применение игрово го программного обеспечения, ну и, конечно же, Интернета Алгоритм планирования потоков существенно влияет на выполнение приложений. С самого начала разработчики Microsott понимали, что его придется изменять по мере того, как будут расширяться сферы применения компьютеров Microsoft гарантирует, что наши программы будут работать и в следующих версиях Windows. Как же ей уда ется изменять внутреннее устройство системы, не нарушая работоспособность наших программ? Ответ в том, что: • планировщик документируется не полностью; • Microsoft не разрешает в полной мере использовать все особенности плани ровщика; • Microsoft предупреждает, что алгоритм работы планировщика постоянно ме няется, и не рекомендует писать программы в расчете на текущий алгоритм Windows API предоставляет слой абстрагирования от конкретного алгоритма ра боты планировщика, запрещая прямое обращение к планировщику. Вместо этого Вы вызываете функции Windows, которые "интерпретируют" Ваши параметры в зависи мости от версии системы, Я буду рассказывать именно об этом слое аборагирования Проектируя свое приложение, Вы должны учитывать возможность параллельного выполнения других программ. Следовательно, Вы обязаны выбирать класс приорите та, исходя из того, насколько "отзывчивой" должна быть Ваша программа. Согласен, такая формулировка довольно туманна, но так и задумано: Microsoft не желает обе щать ничего такого, что могло бы нарушить работу Вашего кода в будущем. Windows поддерживает шесть классов приоритета; idle (простаивающий), below normal (ниже обычного), normal (обычный), above normal (выше обычного), high (вы сокий) и realtime (реального времени). Самый распространенный класс приоритета, естественно, — normal; его использует 99% приложений. Классы приоритета показа ны в следующей таблице.
Приоритет idle идеален для программ, выполняемых, только когда системе боль ше нечего делать, Примеры таких программ — экранные заставки и средства мони торинга. Компьютер, не используемый в интерактивном режиме, может быть занят другими задачами (действуя, скажем, в качестве файлового сервера), и их потокам незачем конкурировать с экранной заставкой за доступ к процессору. Средства мо ниторинга, собирающие статистическую информацию о системе, тоже не должны мешать выполнению более важных задач Класс приоритета high следует использовать лишь при крайней необходимости Может, Вы этого и нс знаете, но Explorer выполняется с высоким приоритетом. Боль шую часть времени его потоки простаивают, готовые пробудиться, кактолько пользо ватель нажмет какую-нибудь клавишу или щелкнет кнопку мыши. Пока потоки Explorer простаивают, система не выделяет им процессорное время, что позволяет выполнять потоки с более низким приоритетом Но вот пользователь нажал, скажем, Ctrl+Esc, и система пробуждает поток Explorer. (Комбинация клавиш Ctrl+Esc попутно открыва ет меню Start.) Если в данный момент исполняются потоки с более низким приори тетом, они немедленно вытесняются, и начинает работать поток Explorer Microsoft разработала Explorer именно так потому, что любой пользователь — независимо от текущей ситуации в системе — ожидает мгновенной реакции оболочки на свои ко манды R сущности, окна Explorcr можно открывать, даже когда все потоки с более низким приоритетом зависают в бесконечных циклах Обладая более высоким при оритетом, потоки Explorer вытесняют поток, исполняющий бесконечный цикл, и дают возможность закрыть зависший процесс. Надо отметить высокую степень продуманности Explorer. Основную часть време ни он просто "спит", не требуя процессорного времени. Будь это не так, вся система работала бы гораздо медленнее, а многие приложения просто не отзывались бы на действия пользователя Классом приоритета real-time почти никогда не стоит пользоваться На самом деле в ранних бета-версиях Windows NT 3.1 присвоение этого класса приоритета прило жениям даже не предусматривалось, хотя операционная система поддерживала эту возможность. Real-time — чрезвычайно высокий приоритет, и, поскольку большин ство потоков в системе (включая управляющие самой системой) имеет более низкий приоритет, процесс с таким классом окажет на них сильное влияние. Так, потоки реального времени могут заблокировать необходимые операции дискового и сетевого ввода-вывода и привести к несвоевременной обработке ввода от мыши и клавиату ры — пользователь может подумать, что система зависла. У Вас должна быть очень веская причина для применения класса real-time — например, программе требуется реагировать на события в аппаратных средствах с минимальной задержкой или вы полнять быстротечную операцию, которую нельзя прерывать ни при каких обстоя тельствах
Конечно, большинство процессов имеет обычный класс приоритета. В Windows 2000 появилось два новых промежуточных класса — below normal и above normal Microsoft добавила их, поскольку некоторые компании жаловались, что существующий набор классов приоритетов не дает должной гибкости. Выбрав класс приоритета, забудьте о том, как Ваша программа будет выполняться совместно с другими приложениями, и сосредоточьтесь на ее потоках. Windows под держивает семь относительных приоритетов потоков: idle (простаивающий), lowcst (низший), below normal (ниже обычного), normal (обычный), above normal (выше обычного), highest (высший) и time-critical (критичный по времени) Эти приорите ты относительны классу приоритета процесса Как обычно, большинство потоков использует обычный приоритет. Относительные приоритеты потоков описаны в сле дующей таблице.
Итак, Вы присваиваете процессу некий класс приоритета и можете изменять от носительные приоритеты потоков в пределах процесса. Заметьте, что я не сказал ни слова об уровнях приоритетов 0-31. Разработчики приложений не имеют с ними дела. Уровень приоритета формируется самой системой, исходя из класса приоритета про цесса и относительного приоритета потока, А механизм его формирования — как раз то, чем Microsoft не хочет себя ограничивать И действительно, этот механизм меня ется практически в каждой версии системы. В следующей таблице показано, как формируется уровень приоритета в Win dows 2000, но не забывайте, что в Windows NT и тем более в Windows 95/98 этот механизм действует несколько иначе Учтите также, что в будущих версиях Windows он вновь изменится. Например, обычный поток в обычном процессе получает уровень приоритета 8, Поскольку большинство процессов имеет класс normal, a большинство потоков — относительный приоритет normal, y основной части потоков в системе уровень при оритета равен 8. Обычный поток в процессе с классом приоритета high получает уровень приори тета 13. Изменив класс приоритета процесса на idle, Вы снизите уровень приоритета того же потока до 4. Вспомните, что приоритет потока всегда относителен классу приоритета его процесса Изменение класса приоритета процесса не влияет на от носительные приоритеты его потоков, но сказывается на уровне их приоритета
Обратите внимание, что в таблице не показано, как задать уровень приоритета 0. Это связано с тем, что нулевой приоритет зарезервирован для потока обнуления стра ниц, и никакой другой поток не может иметь такой приоритет. Кроме того, уровни 17-21 и 27-30 в обычном приложении тоже недоступны. Вы можете пользоваться ими, только если пишете драйвер устройства, работающий в режиме ядра. И еще одно: уровень приоритета потока в процессе с классом real-time не может опускаться ниже 16, а потока в процессе с любым другим классом — подниматься выше 15.
Программирование приоритетовТак как же процесс получает класс приоритета? Очень просто Вызывая CreateProcess, Вы можете указать в ее параметр fdwCreate нужный класс приоритета. Идентифика торы этих классов приведены в следующей таблице.
Вам может показаться странным, что, создавая дочерний процесс, родительский сам устанавливает ему класс приоритета. За примером далеко ходить не надо — возь мем все тот же Explorcr При запуске из него какого-нибудь приложения новый про цесс создается с обычным приоритетом Но Explorer ведь не знает, что делает этот процесс и как часто его потокам надо выделять процессорное время. Поэтому в сис теме предусмотрена возможность изменения класса приоритета самим выполняемым процессом — вызовом функции SetPriontyClass-
Эта функция меняет класс приоритета процесса, определяемого описателем hPro cess, в соответствии со значением параметра fdwPriority. Последний должен содержать одно из значений, указанных в таблице выше. Поскольку SetPriorityClass принимает описатель процесса, Вы можете изменить приоритет любого процесса, выполняемо го в системе, — если его описатель известен и у Вас есть соответствующие права дос тупа. Обычно процесс пытается изменить свой класс приоритета. Вот как процесс может сам себе установить класс приоритета idle:
Парная ей функция GetPriorityClass позволяет узнать класс приоритета любого процесса.
Она возвращает, как Вы догадываетесь, один из ранее перечисленных флагов. При запуске из оболочки командного процессора начальный приоритет програм мы тоже обычный. Однако, запуская ее командой Start, можно указать ключ, опреде ляющий начальный приоритет Так, следующая команда, введенная в оболочке коман дного процессора, заставит систему запустить приложение Calculator и присвоить ему приоритет idle:
Команда Start допускает также ключи /BELOWNORMAL, /NORMAL, /ABOVENORMAL, /HIGH и /REALTIME, позволяющие начать выполнение программы с соответствующим классом приоритета. Разумеется, после запуска программа может вызвать SetPriorrty Class и установить себе другой класс приоритета.
Task Manager в Windows 2000 дает возможность изменять класс приоритета про цесса. На рисунке ниже показана вкладка Processes в окне Task Manager co списком выполняемых на данный момент процессов. В колонке Base Pri сообщается класс приоритета каждого процесся Вы можете изменить его, выбрав процесс и указав другой класс в подменю Set Priority контексшого меню.
Только что созданный поток получает относительный приоритет normal Почему CreateThoread не позволяет задать относительный приоритет — для меня так и остает ся загадкой. Такая операция осуществляется вызовом функции:
Разумеется, параметр bThread указывает на поток, чей приоритет Вы хотите из менить, а через nPriority передается один из идентификаторов (см. таблицу ниже)
Функция GetThreadPnority, парная SetThreadPriority, позволяет узнать относитель ный приоритет потока.
Она возвращает один из идентификаторов, показанных в таблице выше. Чтобы создать поток с относительным приоритетом idle, сделайте, например, так:
Заметьте, что CreateThread всегда создает поток с относительным приоритетом normal. Чтобы присвоить потоку относительный приоритет idle, создайте приоста новленный поток, передав в CreateThread флаг CREATE_SUSPENDED, а потом вызови те SetThreadPriority и установите нужный приоритет. Далее можно вызвать Resume Thread, и поток будет включен в число планируемых. Сказать заранее, когда поток получит процессорное время, нельзя, но планировщик уже учитывает его новый при оритет Выполнив эти операции, Вы можете закрыть описатель потока, чтобы соот ветствующий объект ядра был уничтожен по завершении данного потока.
Динамическое изменение уровня приоритета потокаУровень приоритета, получаемый комбинацией относительного приоритета потока и класса приоритета процесса, которому принадлежит данный поток, называют ба зовым уровнем приоритета потока. Иногда система изменяет уровень приоритета потока Обычно это происходит в ответ на некоторые события, связанные с вводом выводом (например, на появление оконных сообщений или чтение с диска). Так, поток с относительным приоритетом normal, выполняемый в процессе с клас сом приоритета high, имеет базовый приоритет 13 Если пользователь нажимает ка кую-нибудь клавишу, система помещает в очередь потока сообщение WM_KEYDOWN. А поскольку в очереди потока появилось сообщение, поток становится планируемым. При этом драйвер клавиатуры может заставить систему временно поднять уровень приоритета потока с 13 до 15 (действительное значение может отличаться в ту или другую сторону). Процессор исполняет поток в течение отведенного отрезка времени, а по его истечении система снижает приоритет потока на 1, до уровня 14. Далее потоку вновь выделяется квант процессорного времени, по окончании которого система опять снижает уровень приоритета потока на 1. И теперь приоритет потока снова соответ ствует его базовому уровню Текущий уровень приоритета не может быть ниже базового. Кроме того, драйвер устройства, "разбудивший" поток, сам устанавливает величину повышения приори тета. И опять же Microsoft нс документирует, насколько повышаются эти значения кон кретными драйверами. Таким образом, она получает возможность тонко настраивать динамическое изменение приоритетов потоков в операционной системе, чтобы та максимально быстро реагировала на действия пользователя Система повышает приоритет только тех потоков, базовый уровень которых на ходится в пределах 1-15 Именно поэтому данный диапазон называется "областью динамического приоритета" (dynamic priority range). Система не допускает динами ческого повышения приоритета потока до уровней реального времени (более 15) Поскольку потоки с такими уровнями обслуживают системные функции, это ограни чение не дает приложению нарушить работу операционной системы И, кстати, сис тема никогда не меняет приоритет потоков с уровнями реального времени (от 16до 31). Некоторые разработчики жаловались, что динамическое изменение приоритета системой отрицательно сказывается па производительности их приложений, и поэто му Microsoft добавила две функции, позволяющие отключать этот механизм:
SetProcessPriorityBoost заставляет систему включить или oтключить изменение при оритетов всех потоков в указанном процессе, a SetThreadPriorttyBoost действует при менительно к отдельным потокам. Эти функции имеют свои аналоги, позволяющие определять, разрешено или запрещено изменение приоритетов.
Каждой из этих двух функций Вы передаете описатель нужного процесса или потока и адрес переменной чипа BOOL, в которой и возвращается результат.
Есть еще одна ситуация, в которой система динамически повышает приоритет потока Представьте, что поток с приоритетом 4 готов к выполнению, но не может получить доступ к процессору из-за того, что его постоянно занимают потоки с при оритетом 8. Это типичный случай "голодания" потока с более низким приоритетом. Обнаружив такой поток, не выполняемый на протяжении уже трех или четырех се кунд, система поднимает его приоритет до 15 и выделяет ему двойную порцию вре мени По его истечении потоку немедленно возвращается его базовый приоритет. Подстройка планировщика для активного процессаКогда пользователь работает с окнами какого-то процесса, последний считается ак тивным (foreground process), a остальные процессы — фоновыми (background proces ses). Естественно, пользователь заинтересован в повышенной отзывчивости активно го процесса по сравнению с фоновыми Для этого Windows подстраивает алгоритм планирования потоков активного процесса, В Windows 2000, когда процесс становит ся активным, система выделяет его потокам более длительные кванты времени Такая регулировка применяется только к процессам с классом приоритета normal Windows 2000 позволяет модифицировать работу этого механизма подстройки Щелкнув кнопку Performance Options на вкладке Advanced диалогового окна System Properties, Вы открываете следующее окно
Переключатель Applications включает подстройку планировщика для активного процесса, а переключатель Background Services — выключает (в этом случае оптими зируется выполнение фоновых сервисов) В Windows 2000 Professional по умолчанию выбирается переключатель Applications, а в остальных версиях Windows 2000 — пе реключатель Background Services, так как серверы редко используются в интерактив ном режиме Windows 98 тоже позволяет подстраивать распределение процессорного време ни для потоков активного процесса с классом приоритета normal Когда процесс этого класса становится активным, система повышает на 1 приоритет его потоков, если их исходные приоритеты были lowest, below normal, normal, above normal или highest, приоритет потоков idle или time-critical не меняется Поэтому поток с относительным приоритетом normal в активном процессе с классом приоритета normal имеет уро вень приоритета 9, а не 8 Когда процесс вновь становится фоновым, приоритеты его потоков автоматически возвращаются к исходным уровням
Причина для таких изменений активных процессов очень проста система дает им возможность быстрее реагировать на пользовательский ввод Если бы приоритеты их потоков не менялись, то и обычный процесс фоновой печати, и обычный, но актив ный процесс, принимающий пользовательский ввод, — оба одинаково конкурирова ли бы за процессорное время И тогда пользователь, набирая текст в активном при ложении, заметил бы, что текст появляется на экране какими-то рывками Но благо даря тому, что система повышает уровни приоритета потоков активного процесса, они получают преимущество над потоками обычных фоновых процессов Программа-пример Scheduling LabЭта прогрдмма, "07 SchedLab ехе" (см листинг на рис 7-1), позволяет эксперименти ровать с классами приоритетов процессов и относительными приоритетами потоков и исследовать их влияние на общую производительность системы. Файлы исходного кода и ресурсов этой программы находятся в каталоге 07-SchedLab нз компакт-диске, прилагаемом к книге После запуска SchedLab открывается окно, показанное ниже
Изначально первичный поток работает очень активно, и степень использования процессора подскакивает до 100% Все, чем он занимается, — постоянно увеличивает исходное значение на 1 и выводит текущее значение в крайнее справа окно списка Все эти числа не несут никакой смысловой информации; их появление просто демон стрирует, что поюк чем-то занят Чтобы прочувствовать, как повлияет на него изме нение приоритета, запустите по крайней мере два экземпляра программы. Можете также открыть Task Manager и понаблюдать за нагрузкой на процессор, создаваемой каждым экземпляром В начале теста процессор будет загружен на 100%, и Вы увидите, что все экземп ляры SchedLab получают примерно равные кванты процессорного времени (Task Manager должен показать практически одинаковые процентные доли для всех ее эк земпляров.) Как только Вы поднимете класс приоритета одного из экземпляров до above normal или high, львиную долю процессорного времени начнет получать имен но этот экземпляр, а аналогичные показатели для других экземпляров резко упадут. Однако они никогда не опустятся до нуля — это действует механизм динамического повышения приоритета "голодающих" процессов Теперь Вы можете самостоятельно поиграть с изменением классов приоритетов процессов и относительных приорите тов потоков. Возможность установки класса приоритета real-time я исключил наме ренно, чтобы не нарушить paбoтy операционной системы. Если Вы все же хотите поэкспериментировать с этим приоритетом, Вам придется модифицировать исход ный текст моей программы Используя поле Sleep, можно приостановить первичный поток на заданное число миллисекунд в диапазоне oт 0 до 9999 Попробуйте приостанавливать его хотя бы на 1 мс и посмотрите, сколько процессорного времени это позволит сэкономить. На своем ноутбуке с процессором Pentium II 300 МГц, я выиграл аж 99% - впечатляет! Кнопка Suspend заставляет первичный поток создать дочерний поток, который приостанавливает родительский и выводит следующее окно.
Пока это окно открыто, первичный поток полностью отключается от процессо ра, а дочерний тоже не требует процессорного времени, так как ждет от пользовате ля дальнейших действий Вы можете свободно перемещать это окно в пределах экра на или убрать его в сторону от основною окна программы. Поскольку первичный поток остановлен, основное окно не принимает оконных сообщений (в том числе WM_PAINT). Это еще раз доказывает, что поток задержан Закрыв окно с сообщением. Вы возобновите первичный поток, и нагрузка на процессор снова возрастет до 100%. А теперь проведите еще один эксперимент. Откройте диалоговое окно Performance Options (я говорил о нем в предыдущем разделе) и выберите переключатель Back ground Services (или, наоборот, Application) Потом запустите несколько экземпляров мосй программы с классом приоритета normal и выберите один из них, сделав его активным процессом. Вы сможете наглядно убедиться, как эти переключатели влия ют на активные и фоновые процессы Привязка потоков к процессорамПо умолчанию Windows 2000 использует нежесткую привязку (soft affmity) потоков к процессорам Это означает, что при прочих равных условиях, система пытается выполнять поток на том же процессоре, на котором он работал в последний раз При таком подходе можно повторно использовать данные, все еще хранящиеся в кэше процессора В повой компьютерной архитектуре NUMA (Non Uniform MemoryAccess) машина состоит из нескольких плат, на каждой из которых находятся четыре процессора и отдельный банк памяти На следующей иллюстрации показана машина с тремя таки ми платами, в сумме содержащими 12 процессоров Отдельный погок может выпол няться на любом из этих процессоров
Система NUMA достигает максимальной производительности, если процессоры используют память на своей плате Если же они обращаются к памяти на другой пла те, производительность резко падает В такой среде желательно, чтобы потоки одно го процесса выполнялись на процессорах 0-3, другого — на процессорах 4-7 и т д Windows 2000 позволяет подстроиться под эту архитектуру, закрепляя отдельные процессы и потоки за конкретными процессорами Иначе говоря, Вы можете конт ролировать, на каких процессорах будут выполняться Ваши потоки Такая привязка называется жесткой (hard affmity) Количество процессоров система определяет при загрузке, и эта информация ста новится доступной приложениям через функцию GetSystemInfo (о ней — в главе 14). По умолчанию любой поток может выполняться на любом процессоре. Чтобы пото ки отдельного процесса работали лишь на некоем подмножестве процессоров, ис пользуйте функцию SetProcessAffinityMask:
В первом параметре, hProcess, передается описатель процесса. Второй параметр, dwProcessAffinityMask, — это битовая маска, указывающая, на каких процессорах мо гут выполняться потоки данного процесса. Передав, например, значение 0x00000005, мы разрешим процессу использовать только процессоры 0 и 2 (процессоры 1 и 3-31 ему будут недоступны). Привязка к процессорам наследуется дочерними процессами. Так, если для роди тельского процесса задана битовая маска 0x00000005, у всех потоков его дочерних процессов будет идентичная маска, и они смогут работать лишь на тех же процессо рах. Для привязки целой группы процессов к определенным процессорам используйте объект ядра "задание" (см главу 5). Ну и, конечно же, есть функция, позволяющая получить информацию о такой привязке;
Вы передаете ей описатель процесса, а результат возвращается в переменной, на которую указывает pdwProcessAffimtyMask. Кроме того, функция возвращает систем ную маску привязки через переменную, на которую ссылается pdwSystemAffinityMask. Эта маска указывает, какие процессоры в системе могут выполнять потоки. Таким образом, маска привязки процесса всегда является подмножеством системной маски привязки.
До сих пор мы говорили о том, как назначить все потоки процесса определенным процессорам. Но иногда такие ограничения нужно вводить для отдельных потоков. Допустим, в процессе имеется четыре потока, выполняемые на четырехпроцессорной машине. Один из потоков занимается особо важной работой, и Вы, желая повысить вероятность того, что у него всегда будет доступ к вычислительным мощностям, зап рещяете остальным потокам использовать процессор 0. Задать маски привязки для отдельных потоков позволяет функция:
В параметре hTbread передается описатель потока, a dwThreadAffinityMask опреде ляет процессоры, доступные этому потоку. Параметр dwThreadAffinityMask должен быть корректным подмножеством маски привяжи процесса, которому принадлежит данный поток Функция возвращает предыдущую маску привязки потока Вот как ог раничить три потока из нашего примера процессорами 1, 2 и 3
При загрузке система тестирует процессоры типа x86 на наличие в них знамени того "жучка" в операциях деления чисел с плавающей точкой (эта ошибка имеется в некоторых Pentium) Она привязывает поток, выполняющий потенциально сбойную операцию деления, к исследуемому процессору и сравнивает результат с тем, что дол жно быть на самом деле Такая последовательность операций выполняется для каж дого процессора в машине
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||