Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Рихтер Дж., Назар К. - Windows via C C++. Программирование на языке Visual C++ - 2009

.pdf
Скачиваний:
6265
Добавлен:
13.08.2013
Размер:
31.38 Mб
Скачать

240 Часть II. Приступаем к работе

Таков вкратце механизм планирования работы множества потоков. Детали мы обсудим позже, но главное я уже показал. Все очень просто, да? Windows потому и называется системой с вытесняющей многозадачностью, что в любой момент может приостановить любой поток и вместо него запустить другой. Как вы еще увидите, этим механизмом можно управлять, правда, крайне ограниченно. Всегда помните: вы не в состоянии гарантировать, что ваш поток будет выполняться непрерывно, что никакой другой поток не получит доступ к процессору и т. д.

Примечание. Меня часто спрашивают, как сделать так, чтобы поток гарантированно запускался в течение определенного времени после какогонибудь события — например, не позднее чем через миллисекунду после приема данных с последовательного порта? Ответ прост: никак. Такие требования можно предъявлять к операционным системам реального времени, но Windows к ним не относится. Лишь операционная система реального времени имеет полное представление о характеристиках аппаратных Средств, на которых она работает (об интервалах запаздывания контроллеров жестких дисков, клавиатуры и т. д.). А создавая Windows, Майкрософт ставила другую цель: обеспечить поддержку максимально широкого спектра оборудования — различных процессоров, дисковых устройств, сетей и др. Короче говоря, Windows не является операционной системой реального времени. Впрочем, Windows Vista поддерживает некоторые расширения, в некоторых отношениях приближающие ее к ОС реального времени. К таким расширениями относятся службы Thread Ordering (подробнее о ней см. по ссылке http://msdn2.microsoft.com/en-us/library/ms686752.aspx) и Multimedia Class Scheduler для мультимедийных приложений, таких как Windows Media Player 11.

Хочу особо подчеркнуть, что система планирует выполнение только тех потоков, которые могут получать процессорное время, но большинство потоков в системе к таковым не относится. Так, у некоторых объектов-потоков значение счетчика простоев (suspend count) больше 0, а значит, соответствующие потоки приостановлены и не получают процессорное время. Вы може-

Глава 7. Планирование потоков, приоритет и привязка к процессорам.docx 241

те создать приостановленный поток вызовом CreateProcess или CreateThread с флагом CREATE_SUSPENDED. (В следующем разделе я расскажу и о таких функциях, как SuspendThread и ResumeThread.)

Кроме приостановленных, существуют и другие потоки, не участвующие в распределении процессорного времени, — они ожидают каких-либо событий. Например, если вы запускаете Notepad и не работаете в нем с текстом, его поток бездействует, а система не выделяет процессорное время тем, кому нечего делать. Но стоит лишь сместить его окно, прокрутить в нем текст или что-то ввести, как система автоматически включит поток Notepad в число планируемых. Это вовсе не означает, что поток Notepad тут же начнет выполняться. Просто система учтет его при планировании потоков и когда-нибудь выделит ему время — по возможности в ближайшем будущем.

Приостановка и возобновление потоков

В объекте ядра «поток» имеется переменная — счетчик числа простоев данного потока. При вызове CreateProcess или CreateThread он инициализируется значением, равным 1, которое запрещает системе выделять новому потоку процессорное время. Такая схема весьма разумна: сразу после создания поток не готов к выполнению, ему нужно время для инициализации.

После того как поток полностью инициализирован, CreateProcess или CreateThread проверяет, не передан ли ей флаг CREATE_SUSPENDED, и, если да, возвращает управление, оставив поток в приостановленном состоянии. В ином случае счетчик простоев обнуляется, и поток включается в число планируемых — если только он не ждет какого-то события (например, ввода с клавиатуры).

Создав поток в приостановленном состоянии, Вы можете настроить некоторые его свойства (например, приоритет, о котором мы поговорим позже). Закончив настройку, вы должны разрешить выполнение потока. Для этого вызовите ResumeThread и передайте описатель потока, возвращенный функцией CreateThread (описатель можно взять и из структуры, на которую указывает па-

раметр ppiProcInfo, передаваемый в CreateProcess).

DWORD ResumeThread(HANDLE hThread);

Если вызов ResumeThread прошел успешно, она возвращает предыдущее значение счетчика простоев данного потока; в ином случае — 0xFFFFFFFE

Выполнение отдельного потока можно приостанавливать несколько раз. Если поток приостановлен 3 раза, то и возобновлен он должен быть тоже 3 раза — лишь тогда система выделит ему процессорное время. Выполнение потока можно приостановить не только при его создании с флагом CREATE_ SUSPENDED, но и вызовом SuspendThread:

DWORD SuspendThread(HANDLE hThread);

242 Часть II. Приступаем к работе

Любой поток может вызвать эту функцию и приостановить выполнение другого потока (конечно, если его описатель известен). Хоть об этом нигде и не говорится (но я все равно скажу!), приостановить свое выполнение поток способен сам, а возобновить себя без посторонней помощи — нет. Как и ResumeThread, функция SuspendThread возвращает предыдущее значение счетчика простоев данного потока. Поток можно приостанавливать не более чем MAXIMUM_SUSPEND_COUNT раз (в файле WinNT.h это значение определено как 127). Обратите внимание, что SuspendThread в режиме ядра работает асинхронно, но в пользовательском режиме не выполняется, пока поток остается в приостановленном состоянии.

Создавая реальное приложение, будьте осторожны с вызовами SuspendThread, так как нельзя заранее сказать, чем будет заниматься его поток в момент приостановки. Например, он пытается выделить память из кучи и поэтому заблокировал к ней доступ. Тогда другим потокам, которым тоже нужна динамическая память, придется ждать его возобновления. SuspendThread безопасна только в том случае, когда вы точно знаете, что делает (или может делать) поток, и предусматриваете все меры для исключения вероятных проблем и взаимной блокировки потоков. (О взаимной блокировке и других проблемах синхронизации потоков я расскажу в главах 8, 9 и 10.)

Приостановка и возобновление процессов

В Windows понятия «приостановка» и «возобновление» неприменимы к процессам, так как они не участвуют в распределении процессорного времени. Однако меня не раз спрашивали, как одним махом приостановить все потоки определенного процесса. Это можно сделать из другого процесса, причем он должен быть отладчиком и, в частности, вызывать функции вроде WaitForDebugEvent и ContinueDebugEvent. Того же можно добиться с помощью команды Suspend Process

утилиты Process Explorer от Sysinternals (см. http://www.microsoft.com/technet/ sysinternals/ProcessesAndThreads/ProcessExplorer.mspx): она приостанавливает все потоки процесса.

Других способов приостановки всех потоков процесса в Windows нет: программа, выполняющая такую операцию, может «потерять» новые потоки. Система должна как-то приостанавливать в этот период не только все существующие, но и вновь создаваемые потоки. Майкрософт предпочла встроить эту функциональность в системный механизм отладки.

Вам, конечно, не удастся написать идеальную функцию SuspendProcess, но вполне по силам добиться ее удовлетворительной работы во многих ситуациях. Вот мой вариант функции SuspendProcess.

VOID SuspendProcess(DWORD dwProcessID, BOOL fSuspend) {

// получаем список потоков в системе

HANDLE hSnapsnot = CreateToolhelp32Snapshot( TH32CS_SNAPTHREAD, dwProcessID);

Глава 7. Планирование потоков, приоритет и привязка к процессорам.docx 243

if (hSnapshot != INVALID_HANDLE_VALUE) {

// просматриваем список потоков

THREADENTRY32 te = { sizeof(te) };

BOOL f0k = Thread32First(hSnapshot, &te);

for (; f0k; f0k = Thread32Next(hSnapshot, &te)) {

// относится ли данный поток к нужному процессу? if (te.th32OwnerProcessID == dwProcessID) {

// пытаемся получить описатель потока по его идентификатору

HANDLE hThread = OpenThread(THREAD_SUSPEND_RESUME, FALSE, te.th32ThreadID);

if (hThread != NULL) {

// приостанавливаем или возобновляем поток if (fSuspend)

SuspendThread(hThread); else

ResumeThread(hThread);

}

CloseHandle(hThread);

}

}

CloseHandle(hSnapshot);

}

}

Для перечисления списка потоков я использую ToolHelp-функции (они рассматривались в главе 4). Определив потоки нужного процесса, я вызываю

OpenThread:

HANDLE OpenThread(

DWORD dwDesiredAccess,

BOOL bInheritHandle,

DWORD dwThreadID);

Это новая функция, которая появилась в Windows 2000. Она находит объект ядра «поток» по идентификатору, указанному в dwThreadlD, увеличивает его счетчик пользователей на 1 и возвращает описатель объекта. Получив описатель,

я могу передать его в SuspendThread (или ResumeThread).

Вероятно, вы уже догадались, почему SuspendProcess будет срабатывать не во всех случаях: при перечислении могут создаваться новые и уничтожаться существующие потоки. После вызова CreateToolhelp32Snapshot в процессе может появиться новый поток, который моя функция уже не увидит, а значит, и не приостановит. Впоследствии, когда я попытаюсь возобновить потоки, вновь вызвав SuspendProcess, она возобновит поток, который собст-

244 Часть II. Приступаем к работе

венно и не приостанавливался. Но может быть еще хуже: при перечислении текущий поток уничтожается и создается новый с тем же идентификатором. Тогда моя функция приостановит неизвестно какой поток (и даже непонятно в каком процессе).

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

Функция Sleep

Поток может попросить систему не выделять ему процессорное время на определенный период, вызвав:

VOID Sleep(DWORD dwMilliseconds);

Эта функция приостанавливает поток на dwMilliseconds миллисекунд. Отметим несколько важных моментов, связанных с функцией Sleep.

Вызывая Sleep, поток добровольно отказывается от остатка выделенного ему кванта времени.

Система прекращает выделять потоку процессорное время на период, примерно равный заданному. Все верно: если вы укажете остановить поток на 100 мс, приблизительно на столько он и «заснет», хотя не исключено, что его сон продлится на несколько секунд или даже минут больше. Вспомните, Windows не является системой реального времени. Ваш поток может возобновиться в заданный момент, но это зависит от того, какая ситуация сложится в системе к тому времени.

Вы можете вызвать Sleep и передать в dwMilliseconds значение INFINITE, вообще запретив планировать поток. Но это не очень практично — куда лучше корректно завершить поток, освободив его стек и объект ядра.

Вы можете вызвать Sleep и передать в dwMilliseconds нулевое значение. Тогда вы откажетесь от остатка своего кванта времени и заставите систему подключить к процессору другой поток. Однако система может снова запустить ваш поток, если других планируемых потоков с тем же приоритетом нет.

Переключение потоков

Функция SwitchToThread позволяет подключить к процессору другой поток (если он есть):

BOOL SwitchToThread();

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

Глава 7. Планирование потоков, приоритет и привязка к процессорам.docx 245

у вызывающего). По истечении этого кванта планировщик возвращается в обычный режим работы.

SwitchToThread позволяет потоку, которому не хватает процессорного времени, отнять этот ресурс у потока с более низким приоритетом. Она возвращает FALSE, если на момент ее вызова в системе нет ни одного потока, готового к исполнению; в ином случае — ненулевое значение.

Вызов SwitchToThread аналогичен вызову Sleep с передачей в dwMilliseconds

нулевого значения. Разница лишь в том, что SwitchToThread дает возможность выполнять потоки с более низким приоритетом, которым не хватает процессорного времени, a Sleep действует без оглядки на «голодающие» потоки.

Переключение потоков на компьютерах с процессором, поддерживающим HyperThreading

Чипы процессоров Xeon, некоторых Pentium 4 и более поздних моделей содержат несколько «логических процессоров», каждый из которых способен исполнять потоки (такие процессоры поддерживают технологию HyperThreading, HT). У каждого потока имеется отдельное состояние (набор регистров процессора), но основные ресурсы, такие как кэш процессора, все потоки используют сообща. Когда исполнение одного из потоков приостанавливается, процессор автоматически переходит к исполнению следующего потока без участия операционной системы. Такие паузы возникают при промахах кэша, ошибках в предсказании ветвления, ожидании результатов предыдущей команды и в некоторых других обстоятельствах. По утверждению Intel, НТ-процессоры работают на 10-30% быстрее, в зависимости от приложения и алгоритма работы с памятью. Подробнее о НТ-

процессорах см. по ссылке http://www.micmsoft.com/whdc/system/CEC/HTWindows.mspx.

При исполнении циклов на НТ-процессорах необходимо принудительно приостанавливать исполнение текущего потока, чтобы дать доступ к процессору и другим потокам. Процессор типа х86 поддерживает инструкцию ассемблера PAUSE. Эта инструкция предотвращает нарушение порядка доступа к памяти, повышает производительность системы и снижает энергопотребление. В х86 эквивалентом PAUSE являются инструкция REP NOP, обеспечивающая совместимость с прежними моделями процессоров с архитектурой IA-32, не поддерживающими HyperThreading. PAUSE заставляет процессор сделать паузу конечной длительности (на некоторых моделях это пауза нулевой длины). В Win32 API инструкцию PAUSE генерирует вызов макроса YieldProcessor, объявленного в WinNT.h. Этот макрос позволяет разработчикам писать код, независимый от архитектуры процессора, а также позволяет избежать лишних вызовов функций.

246 Часть II. Приступаем к работе

Определение периодов выполнения потока

Иногда нужно знать, сколько времени затрачивает поток на выполнение той или иной операции. Многие в таких случаях, используя новую функцию GetTtckCount64, пишут что-то вроде этого:

// получаем стартовое время

ULONGLONG qwStartTlne = GetTickCount64();

//здесь выполняем какой-нибудь сложный алгоритм

//вычитаем стартовое время из текущего

ULONGLONG qwElapsedTime = GetTickCount64() - qwStartTime;

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

BOOL GetThreadTimes(

HANDLE hThread,

PFILETIME pftCreationTime,

PFILETIME pftExitTime,

PFILETIME pftKernelTime,

PFILETIME pftUserTime);

GetThreadTimes возвращает четыре временных параметра (см. табл. 7-1).

Табл. 7-1. Значения, возвращаемые GeetThreadTimes

Показатель времени

Описание

Время создания

Абсолютная величина, выраженная в интервалах по 100 не. Отсчиты-

вается с полуночи 1 января 1601 года по Гринвичу до момента созда-

(creation time)

ния потока

 

 

Абсолютная величина, выраженная в интервалах по 100 не. Отсчиты-

Время завершения

вается с полуночи 1 января 1601 года по Гринвичу до момента завер-

(exit time)

шения потока. Если поток все еще выполняется, этот показатель имеет

 

неопределенное значение

Время выполнения

Относительная величина, выраженная в интервалах по 100 не. Сооб-

щает время, затраченное этим потоком на выполнение кода операци-

ядра (kernel time)

онной системы

 

Время выполнения

Относительная величина, выраженная в интервалах по 100 не. Сооб-

User (User time)

щает время, затраченное потоком на выполнение кода приложения

С помощью этой функции можно определить время, необходимое для выполнения сложного алгоритма:

Глава 7. Планирование потоков, приоритет и привязка к процессорам.docx 247

__int64 FileTimeToQuadWord (PFILETIME pft) { return(Int64ShllMod32(pft->dwHighDateTime, 32) | pft->dwLowDateTime);

}

void PerformLongOperation () {

FILETIME ftKernelTimeStart, ftKernelTimeEnd; FILETIME ftUserTimeStart, ftUserTimeEnd; FILETIME ftDummy;

__int64 qwKernelTimeElapsed, qwUserTimeElapsed, qwTotalTimeElapsed;

//получаем начальные показатели времени

GetThreadTimes(GetCurrentThread(), &ftDummy, &ftDummy, &ftKernelTimeStart, &ftUserTimeStart);

//здесь выполняем сложный алгоритм

//получаем конечные показатели времени

GetThreadTimes(GetCurrentThread(), &ftDummy, &ftDummy, &ftKernelTimeEnd, &ftUserTimeEnd);

//Получаем значения времени, затраченного на выполнение ядра и

//User, преобразуя начальные и конечные показатели времени из

//FILETIME в учетверенные слова, вычитая затем начальные

//показатели из конечных.

qwKernelTimeElapsed = FileTimeToQuadWord(&ftKernelTimeEnd) – FileTimeToQuadWord(&ftKernelTimeStart);

qwUserTimeElapsed = FileTimeToQuadWord(&ftUserTimeEnd) – FileTimeToQuadWord(&ftUserTimeStart);

//получаем общее время, складывая время выполнения ядра и User qwTotalTimeElapsed = qwKernelTimeElapsed + qwUserTimeElapsed;

//общее время хранится в qwTotalTimeElapsed

}

Заметим, что существует еще одна функция, аналогичная GetThreadTimes и применимая ко всем потокам в процессе:

BOOL GetProcessTimes(

HANDLE hProcess,

PFILETIME pftCreationTime,

PFILETIME pftExitTime,

PFILETIME pftKernelTime,

PFILETIME pftUserTime);

248 Часть II. Приступаем к работе

GetProcessTimes возвращает временные параметры, суммированные по всем потокам (даже уже завершенным) в указанном процессе. Так, время выполнения ядра будет суммой периодов времени, затраченного всеми потоками процесса на выполнение кода операционной системы.

В Windows Vista изменился способ подсчета времени процессора, использованного потоком. Теперь вместо таймера, отсчитывающего время интервалами по

10-15 мс (о нем и утилите ClockRes см. http://www.microsoft.com/technet/sysinternals/information/higresolutiontimers.mspx операционная система используется 64-

разрядный аппаратный механизм Time Stamp Counter (TSC). TSC поддерживается процессором и отсчитывает число тактов с момента включения компьютера. Несложно представить, насколько этот механизм, работающий в системе с процессором, таковая частота которого измеряется в МГц, точнее таймера, «цена деления» которого — несколько миллисекунд.

Когда планировщик останавливает исполнение потока, вычисляется разность между текущим значением TSC и его значением на момент начала кванта, результат прибавляется к общему времени исполнения потока. При этом, в отличие от предыдущих версий Windows, пауза не учитывается. Функции

QueryThreadCycleTime и QueryProcessCycleTime возвращают число тактов, ис-

пользованных заданным потоком или всеми потоками данного процесса, соответственно. Если вы хотите заменить GetTickCount64 более точным «хронометром», можно получать текущие значения TSC вызовом ReadTimeStampCounter макроса, объявленного в WinNT.h, использующего внутреннюю функцию __rdtsc компилятора C++.

GetThreadTimes не годится для высокоточного измерения временных интервалов — для этого в Windows предусмотрено две специальные функции:

BOOL QueryPerformanceFrequency(LARGE_INTEGER* pliFrequency);

BOOL OueryPerfomanceCounter(LARGE_INTEGER* pliCount);

Они построены на том допущении, что поток не вытесняется, поскольку высокоточные измерения проводятся, как правило, в очень быстро выполняемых блоках кода. Чтобы слегка упростить работу с этими функциями, я создал следующий С++-класс:

class CStopwatch { public:

CStopwatch() { QueryPerfor»anc6Frequency(&m_liPerfFreq); Start(); } void Start() { QueryPerformanceCounter(&m_liPerfStart); }

__int64 Now() const { // возвращает число миллисекунд после вызова Start

LARGE_INTEQER liPerfNow; QueryPerformanceCounter (&liPerfNow);

return(((liPerfNow.QuadPart - m_liPerfStart.QuadPart) * 1000)

Глава 7. Планирование потоков, приоритет и привязка к процессорам.docx 249

/ m_liParfFreq.QuadPart);

}

 

__int64 NowInMicro() const {

// возвращает чиcло миллисекунд

 

// после вызова Start

LARGE_INTEGER liPerfNow;

 

QueryPerfornanceCounter(&liPerfNow); return(((liPerfNow.QuadPart – m_liPerfStart.QuadPart) * 1000000)

/ m_liPerfFreq.QuadPart);

}

 

private:

 

LARGE_INTEGER m_liPerfFreq;

// количество отсчетов в секунду

LARGE_INTEGER m_liPerfStart;

// начальный отсчет

};

 

Яприменяю этот класс так:

//создаю секундомер (начинающий отсчет с текущего момента времени)

CStopwatch stopwatch;

//здесь я помещаю код, время выполнения которого нужно измерить

//определяю, сколько времени прошло

__int64 qwElapsedTime = stopwatch.Now();

// qwElapsedTime сообщает длительность выполнения в миллисекундах

Функции для работы с высокоточным таймером удобны для преобразования значений, возвращаемых новыми Get*CycleTime-функциями. Поскольку длина такта (а значит, и результаты измерения) зависят от тактовой частоты процессора, для преобразования числа тактов в более информативную величину необходимо узнать тактовую частоту. Так, 2-ГГц процессор совершает 2 млрд. тактов в секунду, следовательно, 800 000 тактов этого процессора будут соответствовать 0,4 мс, тогда как на более медленном 1-ГГц процессоре то же число тактов соответствует 0,8 мс. Вот реализация функции GetCPUFrequencylnMHz:

DWORD GetCPUFrequencylnMHz() {

//Изменяем приоритет потока, чтобы у него было больше шансов на

//на подключение к процессору по окончании Sleep().

int currentPriority = GetThreadPriority(GetCurrentThread()); SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_HIGHEST);

//отсчет времени с помощью другого таймера

__int64 elapsedTime = 0;

//создаем секундомер (по умолчанию он отсчитывает время, начиная

стекущего момента).

Соседние файлы в предмете Программирование на C++