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

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

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

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

такие ситуации.) Наконец, в случае ожидаемых таймеров вы с большей вероятностью будете получать уведомления именно по истечении заданного интервала. Сообщения WM_TIMER всегда имеют наименьший приоритет и принимаются, только когда в очереди потока нет других сообщений. Но ожидаемый таймер обрабатывается так же, как и любой другой объект ядра: если он сработал, ждущий поток немедленно пробуждается.

Семафоры

Объекты ядра «семафор» используются для учета ресурсов. Как и все объекты ядра, они содержат счетчик числа пользователей, но, кроме того, поддерживают два 32-битных значения со знаком: одно определяет максимальное число ресурсов (контролируемое семафором), другое используется как счетчик текущего числа ресурсов.

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

Изначально, когда запросов от клиентов еще нет, сервер не разрешает выделять процессорное время каким-либо потокам в пуле. Но как только серверу поступает, скажем, три клиентских запроса одновременно, три потока в пуле становятся планируемыми, и система начинает выделять им процессорное время. Для слежения за ресурсами и планированием потоков семафор очень удобен. Максимальное число ресурсов задается равным 5, что соответствует размеру буфера. Счетчик текущего числа ресурсов первоначально получает нулевое значение, так как клиенты еще не выдали ни одного запроса. Этот счетчик увеличивается на 1 в момент приема очередного клиентского запроса и на столько же уменьшается, когда запрос передается на обработку одному из серверных потоков в пуле.

Для семафоров определены следующие правила:

когда счетчик текущего числа ресурсов становится больше 0, семафор переходит в свободное состояние;

если этот счетчик равен 0, семафор занят;

система не допускает присвоения отрицательных значений счетчику текущего числа ресурсов;

счетчик текущего числа ресурсов не может быть больше максимального числа ресурсов.

Глава 9. Синхронизация потоков с использованием объектов ядра.docx 307

Не путайте счетчик текущего числа ресурсов со счетчиком числа пользователей объекта-семафора.

HANDLE CreateSemaphore(

PSECURITY_ATTRIBUTE psa,

L0NG lInitialCount,

LONG lMaximumCount,

PCTSTR pszName);

О параметрах psa и pszName я рассказывал в главе 3. Следующая функция позволяет напрямую предоставить права доступа, указав их в параметре dwDesiredAccess. Заметьте, что параметр dwFlags зарезервирован и должен быть установлен в 0.

HANDLE CreateSemaphoreEx(

PSECURnY_ATTRIBUTES psa,

L0NG lInitialCount,

LONG lMaximumCount,

PCTSTR pszName,

DWORD dwFlags,

DWORD dwDesiredAccess);

Разумеется, любой процесс может получить свой («процессозависимый») описатель существующего объекта «семафор», вызвав OpenSemaphore.

HANDLE OpenSemaphore(

DWORD dwDesiredAccess,

BOOL bInheritHandle,

PCTSTR pszName);

Параметр lMaximumCount сообщает системе максимальное число ресурсов, обрабатываемое вашим приложением. Поскольку это 32-битное значение со знаком, предельное число ресурсов может достигать 2 147 483 647. Параметр lInitialCount указывает, сколько из этих ресурсов доступно изначально (наданный момент). При инициализации моего серверного процесса клиентских запросов нет, поэтому я вызываю CreateSemaphore так:

HANDLE hSemaphore = CreateSemaphore(NULL, 0, 5, NULL);

Это приводит к созданию семафора со счетчиком максимального числа ресурсов, равным 5, при этом изначально ни один ресурс не доступен. (Кстати, счетчик числа пользователей данного объекта ядра равен 1, так как я только что создал этот объект; не запутайтесь в счетчиках.) Поскольку счетчику текущего числа ресурсов присвоен 0, семафор находится в занятом состоянии. А это значит, что любой поток, ждущий семафор, просто засыпает.

Поток получает доступ к ресурсу, вызывая одну из Wait-функций и передавая ей описатель семафора, который охраняет этот ресурс. Wait-функция проверяет у семафора счетчик текущего числа ресурсов: если его значение больше 0 (семафор свободен), уменьшает значение этого счетчика на 1, и

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

вызывающий поток остается планируемым. Очень важно, что семафоры выполняют эту операцию проверки и присвоения на уровне атомарного доступа; иначе говоря, когда вы запрашиваете у семафора какой-либо ресурс, операционная система проверяет, доступен ли этот ресурс, и, если да, уменьшает счетчик текущего числа ресурсов, не позволяя вмешиваться в эту операцию другому потоку. Только после того как счетчик ресурсов будет уменьшен на 1, доступ к ресурсу сможет запросить другой поток.

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

Поток увеличивает значение счетчика текущего числа ресурсов, вызывая функцию ReleaseSemaphore:

BOOL ReleaseSemaphore(

HANDLE hSemaphore,

LONG lReleaseCount,

PLONG plPreviousCount);

Она просто складывает величину lReleaseCount со значением счетчика текущего числа ресурсов. Обычно в параметре lReleaseCount передают 1, о это вовсе не обязательно: я часто передаю в нем значения, равные или большие 2. Функция возвращает исходное значение счетчика ресурсов в *plPreviousCount. Если вас не интересует это значение (а в большинстве программ так оно и есть), передайте в параметре *plPreviousCount значение NULL.

Было бы удобнее определять состояние счетчика текущего числа ресурсов, не меняя его значение, но такой функции в Windows нет. Поначалу я думал, что вызовом ReleaseSemaphore с передачей ей во втором параметре нуля можно узнать истинное значение счетчика в переменной типа LONG, на которую указывает параметр *plPreviousCount. Но не вышло: функция занесла туда нуль. Я передал во втором параметре заведомо большее число, и — тот же результат. Тогда мне стало ясно: получить значение этого счетчика, не изменив его, невозможно.

Мьютексы

Объекты ядра «мьютексы» гарантируют потокам взаимоисключающий доступ к единственному ресурсу. Отсюда и произошло название этих объектов (mutual exclusion, mutex). Они содержат счетчик числа пользователей, счетчик рекурсии и переменную, в которой запоминается идентификатор поттока. Мьютексы ведут себя точно так же, как и критические секции. Но если последние являются объектами пользовательского режима (см. главу 8), то мьютексы — объектами ядра. Кроме того, единственный объект-мьютекс позволяет синхронизировать доступ к ресурсу нескольких потоков из раз-

Глава 9. Синхронизация потоков с использованием объектов ядра.docx 309

ных процессов; при этом можно задать максимальное время ожидания доступа к ресурсу.

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

Для мьютексов определены следующие правила:

если его идентификатор потока равен 0 (у самого потока не может быть такой идентификатор), мьютекс нс захвачен ни одним из потоков и находится в свободном состоянии;

если его идентификатор потока не равен 0, мьютекс захвачен одним из потоков и находится в занятом состоянии;

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

Для использования объекта-мьютекса один из процессов должен сначала создать его вызовом CreateMutex.

HANDLE CreateMutex(

PSECURITY_ATTRIBUTES psa,

B00L MnitialOwner,

PCTSTR pszName);

О параметрах psa и pszName я рассказывал в главе 3. Следующая функция позволяет напрямую передать права доступа, указав их в параметре dwDesiredAccess. Параметр dwFlags — замена параметра bInitialOwned функции

CreateMutex: 0 означает FALSE, а CREATE_MUTEX_INITIAL_OWNER — TRUE.

HAN0LE CreateHutexEx(

PSECURITY_ATTRIBUTES psa,

PCTSTR pszName,

DWORD dwFlags,

DWORD dwDesiredAccess);

Разумеется, любой процесс может получить свой («процессозависимый») описатель существующего объекта «мьютекс», вызвав OpenMutex.

HANDLE OpenMutex(

DWORD dwDesiredAccess,

B00L bInheritHandle,

PCTSTR pszName);

Параметр bfInitialOwner определяет начальное состояние мьютекса. Если в нем передается FALSE (что обычно и бывает), объект-мьютекс не прина-

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

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

Поток получает доступ к разделяемому ресурсу, вызывая одну из Waitфункций и передавая ей описатель мьютекса, который охраняет этот ресурс. Waitфункция проверяет у мьютекса идентификатор потока: если его значение не равно 0, мьютекс свободен; в ином случае оно принимает значение идентификатора вызывающего потока, и этот поток остается планируемым.

Если Wait-функция определяет, что у мьютекса идентификатор потока не равен 0 (мьютекс занят), вызывающий поток переходит в состояние ожидания. Система запоминает это и, когда идентификатор обнуляется, записывает в него идентификатор ждущего потока, а счетчику рекурсии присваивает значение 1, после чего ждущий поток вновь становится планируемым. Все проверки и изменения состояния объекта-мьютекса выполняются на уровне атомарного доступа.

Для мьютексов сделано одно исключение в правилах перехода объектов ядра из одного состояния в другое. Допустим, поток ждет освобождения занятого объ- екта-мьютекса. В этом случае поток обычно засыпает (переходит в состояние ожидания). Однако система проверяет, не совпадает ли идентификатор потока, пытающегося захватить мьютекс, с аналогичным идентификатором у мьютекса. Если они совпадают, система по-прежнему выделяет потоку процессорное время, хотя мьютекс все еще занят. Подобных особенностей в поведении нет ни у каких других объектов ядра в системе. Всякий раз, когда поток захватывает объектмьютекс, счетчик рекурсии в этом объекте увеличивается на 1. Единственная ситуация, в которой значение счетчика рекурсии может быть больше 1, — поток захватывает один и тот же мьютекс несколько раз, пользуясь упомянутым исключением из общих правил.

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

B00L ReleaseMutex(HANDLE hMutex);

Эта функция уменьшает счетчик рекурсии в объекте-мьютексе на 1. Если данный объект передавался во владение потоку неоднократно, поток обязан вызвать ReleaseMutex столько раз, сколько необходимо для обнуления счетчика рекурсии. Как только счетчик станет равен 0, переменная, хранящая идентификатор потока, тоже обнулится, и объект-мьютекс освободится. После этого система проверит, ожидают ли освобождения мьютекса ка- кие-нибудь другие потоки. Если да, система «по-честному» выберет один из

Глава 9. Синхронизация потоков с использованием объектов ядра.docx 311

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

Отказ от объекта-мьютекса

Объект-мьютекс отличается от остальных объектов ядра тем, что занявшему его потоку передаются права на владение им. Прочие объекты могут быть либо свободны, либо заняты — вот, собственно, и все. А объекты-мьютексы способны еще и запоминать, какому потоку они принадлежат. Именно поэтому для мьютексов сделано исключение: поток может завладеть даже занятым мьютексом.

Особенности мьютексов касаются не только их захвата, но и освобождения. Если какой-то посторонний поток попытается освободить мьютекс вызовом функции ReleaseMutex, то она, проверив идентификаторы потоков и обнаружив их несовпадение просто вернет FALSE. Тут же вызвав GetLastError, вы получите значение ERROR_NOT_OWNER.

Отсюда возникает вопрос: а что будет, если поток, которому принадлежит мьютекс, завершится, не успев его освободить? В таком случае система считает, что произошел отказ от мьютекса, и автоматически переводит его в свободное состояние (сбрасывая при этом все его счетчики в исходное состояние). Система отслеживает все мьютексы и объекты ядра «поток», поэтому она точно знает, когда поток отказывается от мьютекса. У таких мьютексов система автоматически сбрасывает идентификатор потока-владельца и счетчик рекурсии до 0. Если этот мьютекс ждут другие потоки, система, как обычно, «по-честному» выбирает один из потоков и позволяет ему захватить мьютекс, записывая в мьютекс идентификатор этого потока и увеличивая счетчик рекурсии до 1. В результате этот поток становится планируемым.

Тогда Wait-функция возвращает потоку WAIT_ABANDONED вместо WAIT_OBJECT_0, и тот узнает, что мьютекс освобожден некорректно. Данная ситуация, конечно, не самая лучшая. Выяснить, что сделал с защищенными данными завершенный поток — бывший владелец объекта-мьютекса, увы, невозможно. Не исключено, что они необратимо повреждены. Что должно делать приложение в таких случаях — решать вам.

В реальности программы никогда специально нс проверяют возвращаемое значение на WAIT_ABANDONED, потому что такое завершение потоков происходит очень редко. (Вот, кстати, еще один яркий пример, доказывающий, что вы не должны пользоваться функцией TerminateThread.)

Мьютексы и критические секции

Мьютексы и критические секции одинаковы в том, как они влияют на планирование ждущих потоков, но различны по некоторым другим характеристикам. Эти объекты сравниваются в следующей таблице.

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

Табл. 9-2. Сравнение мьютексов и критических секций

Характеристики

Объект-мьютекс

Объект — критическая сек-

ция

 

 

Быстродействие

Малое

Высокое

Возможность использова-

 

 

ния за границами процес-

Да

Нет

са

 

 

Объявление

HANDLE hmtx

CRITICAL_SECTION cs;

Инициализация

hmtx = CreateMutex (NULL,

InitializeCriticalSection (&cs);

 

FALSE, NULL);

 

 

Очистка

CloseHandle(hmtx);

DeleteCriticalSection(&cs);

Бесконечное ожидание

WaitForSingleObject (hmtx, INFI-

EnterCriticalSection(&cs);

 

NITE);

 

 

Ожидание в течение 0 мс

WaitForSingleObject (hmtx, 0);

TryEnterCriticalSection(&cs);

Ожидание в течение про-

WaitForSingleObject (hmtx, dwMil-

 

извольного периода вре-

Невозможно

liseconds);

мени

 

 

 

Освобождение

ReleaseMutex(hmtx);

LeaveCriticalSection (&cs);

Возможность параллель-

Да (с помощью WaitForMultip-

 

ного ожидания других

leObjects или аналогичной функ-

Нет

объектов ядра

ции)

 

Программа-пример Queue

В главе 8 на примере одноименной программы я продемонстрировал управление очередью с помощью SRWLock и условных переменных. В этой главе мы рассмотрим версию Queue (09 Queue.exe, исходный текст и файлы ресурсов см. в каталоге 09-Queue архива, доступного на веб-сайте поддержки книги). Эта программа безопасна в многопоточной среде, кроме того, ею проще манипулировать из других потоков. Она управляет очередью обрабатываемых элементов данных, используя мьютекс и семафор. Файлы исходного кода и ресурсов этой программы находятся в каталоге 09-Queue на компакт-диске, прилагаемом к книге. После запуска Queue открывается окно, показанное ниже.

Как и в примере из 8 главы, при инициализации Queue создает четыре клиентских и два серверных потока. Каждый клиентский поток засыпает на определенный период времени, а затем помещает в очередь элемент данных. Когда в очередь ставится новый элемент, содержимое списка Client Threads обновляется. Каждый элемент данных состоит из номера клиентского потока и порядкового номера запроса, выданного этим потоком. Например, первая запись в списке сообщает, что клиентский поток 0 поставил в очередь свой первый запрос. Следующие записи свидетельствуют, что далее свои

Глава 9. Синхронизация потоков с использованием объектов ядра.docx 313

первые запросы выдают потоки 1-3, потом поток 0 помещает второй запрос, то же самое делают остальные потоки, и все повторяется.

Серверные потоки ничего не делают, пока в очереди не появится хотя бы один элемент данных. Как только он появляется, для его обработки пробуждается один из серверных потоков. Состояние серверных потоков отражается в списке Server Threads. Первая запись говорит о том, что первый запрос от клиентского потока 0 обрабатывается серверным потоком 0, вторая запись — что первый запрос от клиентского потока 1 обрабатывается серверным потоком 1, и т. д.

В этом примере серверные потоки не успевают обрабатывать клиентские запросы и очередь в конечном счете заполняется до максимума. Я установил максимальную длину очереди равной 10 элементам, что приводит к быстрому заполнению этой очереди. Кроме того, на четыре клиентских потока приходится лишь два серверных. В итоге очередь полностью заполняется к тому моменту, когда клиентский поток 3 пытается выдать свой пятый запрос.

Итак, что делает программа, вы поняли; теперь посмотрим — как она это делает (что гораздо интереснее). Очередью управляет С++-класс CQueue:

class CQueue { public:

Struct ELEMENT {

int m_nThreadNum, m_nRequestNum;

// Другие элементы данных должны быть определены здесь

};

typedef ELEMENT* PELEMENT;

private:

 

 

PELEMENT m_pElements;

// массив элементов, подлежащих обработке

int

m_nMaxElements;

// количество элементов в массиве

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

HANDLE

m_h[2];

// описатели мьютекса и семафора

HANDLE

&m_hmtxQ;

// ссылка на m_h[0]

HANDLE

&m_hsemNumElements;

// ссылка на m_h[1]

public:

CQueue(int nMaxElements); ~CQueue();

BOOL Append(PELEMENT pElement, DWORD dwMilliseconds); BOOL Remove(PELEMENT pElement, DWORD dwMilliseconds);

};

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

Что касается закрытых элементов класса, мы имеем m_pElements, который указывает на массив (фиксированного размера) структур ELEMENT. Эти данные как раз и нужно защищать от одновременного доступа к ним со стороны клиентских и серверных потоков. Элемент m_nMaxElements определяет размер массива при создании объекта CQueue. Следующий элемент, m_h, — это массив из двух описателей объектов ядра. Для корректной защиты элементов данных в очереди нам нужно два объекта ядра: мьютекс и семафор. Эти два объекта создаются в конструкторе CQueue; в нем же их описатели помещаются в массив m_h.

Как вы вскоре увидите, программа периодически вызывает WaitForMultipleObjects, передавая этой функции адрес массива описателей. Вы также убедитесь, что программе время от времени приходится ссылаться только на один из этих описателей. Чтобы облегчить чтение кода и его модификацию, я объявил два элемента, каждый из которых содержит ссылку на один из описателей, — m_hmtxQ и m_hsemNumElements. Конструктор CQueue инициализирует эти элементы содержимым m_h[0] и m_h[1] соответственно.

Теперь вы и сами без труда разберетесь в методах конструктора и деструктора CQueue, поэтому я перейду сразу к методу Append. Этот метод пытается добавить ELEMENT в очередь. Но сначала он должен убедиться, что вызывающему потоку разрешен монопольный доступ к очереди. Для этого метод Append вызывает WaitForSingleObject, передавая ей описатель объекта-мьютекса, m_hmtxQ. Если функция возвращает WAIT_OBJECT_0, значит, поток получил монопольный доступ к очереди.

Далее метод Append должен попытаться увеличить число элементов в очереди, вызвав функцию ReleaseSemaphore и передав ей счетчик числа освобождений (release count), равный 1. Если вызов ReleaseSemaphore прохо-

Глава 9. Синхронизация потоков с использованием объектов ядра.docx 315

дит успешно, в очереди еще есть место, и в нее можно поместить новый элемент. К счастью, ReleaseSemaphore возвращает в переменной lPreviousCount предыдущее количество элементов в очереди. Благодаря этому вы точно знаете, в какой элемент массива следует записать новый элемент данных. Скопировав элемент в массив очереди, функция возвращает управление. По окончании этой операции Append вызывает ReleaseMutex, чтобы и другие потоки могли получить доступ к очереди. Остальной код в методе Append отвечает за обработку ошибок и неудачных вызовов.

Теперь посмотрим, как серверный поток вызывает метод Remove для выборки элемента из очереди. Сначала этот метод должен убедиться, что вызывающий поток получил монопольный доступ к очереди и что в ней есть хотя бы один элемент. Разумеется, серверному потоку нет смысла пробуждаться, если очередь пуста. Поэтому метод Remove предварительно обращается к WaitForMultipteObjects, передавая ей описатели мьютекса и семафора. И только после освобождения обоих объектов серверный поток может пробудиться.

Если возвращается WAIT_OBJECT_0, значит, поток получил монопольный доступ к очереди и в ней есть хотя бы один элемент. В этот момент программа извлекает из массива элемент с индексом 0, а остальные элементы сдвигает вниз на одну позицию. Это, конечно, не самый эффективный способ реализации очереди, так как требует слишком большого количества операций копирования в памяти, но наша цель заключается лишь в том, чтобы продемонстрировать синхронизацию потоков. По окончании этих операций вызывается ReleaseMutex, и очередь становится доступной другим потокам.

Заметьте, что объект-семафор отслеживает, сколько элементов находится в очереди. Вы, наверное, сразу же поняли, что это значение увеличивается, когда метод Append вызывает ReleaseSemaphore после добавления нового элемента к очереди. Но как оно уменьшается после удаления элемента из очереди, уже не столь очевидно. Эта операция выполняется вызовом WaitForMultipleObjects из метода Remove. Тут надо вспомнить, что побочный эффект успешного ожидания семафора заключается в уменьшении его счетчика на 1. Очень удобно для нас.

Теперь, когда вы понимаете, как работает класс CQueue, вы легко разберетесь в остальном коде этой программы.

Queue.cpp

/********************************************************************

Module: Queue.cpp

Notices: Copyright (с) 2008 Jeffrey Richter & Christophe Nasarre

********************************************************************/

#include

“..CommonFiles\CmhHdr.h"

/* см. приложение А */

#include

<windowsx.h>

 

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