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

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

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

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

Побочные эффекты успешного ожидания

Успешный вызов WaitForSingleObject или WaitForMultipleObjects на самом деле меняет состояние некоторых объектов ядра. Под успешным вызовом я имею в виду тот, при котором функция видит, что объект освободился, и возвращает значение, относительное WAIT_OBJECT_0. Вызов считается неудачным, если возвращается WAIT_TIMEOUT или WAIT_FAILED. В последнем случае состояние ка- ких-либо объектов не меняется.

Изменение состояния объекта в результате вызова я называю побочным эф-

фектом успешного ожидания (successful wait side effect). Например, поток ждет объект «событие с автосбросом» (auto-reset event object) (об этих объектах я расскажу чуть позже). Когда объект переходит в свободное состояние, функция обнаруживает это и может вернуть вызывающему потоку значение WAIT_OBJECT_0. Однако перед самым возвратом из функции событие переводится в занятое состояние — здесь сказывается побочный эффект успешного ожидания.

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

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

Возьмем такой пример. Два потока вызывают WaitForMultipleObjects совершенно одинаково:

HANDLE h[2];

 

h[0] = hAutoResetEvent1;

// изначально занят

n[1] - hAutoResetEvent2;

// изначально занят

WaitForMultipleObjects(2, h, TRUE, INFINITE);

На момент вызова WaitForMultipleObjects эти объекты-события заняты, и оба потока переходят в режим ожидания. Но вот освобождается объект hAutoResetEvent1. Это становится известным обоим потокам, однако ни один из них не пробуждается, так как объект hAutoResetEvent2 по-прежнему занят. Поскольку потоки все еще ждут, никакого побочного эффекта для объекта hAutoResetEvent1 не возникает.

Наконец освобождается и объект hAutoResetEvent2. В этот момент один из потоков обнаруживает, что освободились оба объекта, которых он ждал. Его ожидание успешно завершается, оба объекта снова переводятся в заня-

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

тое состояние, и выполнение потока возобновляется. А что же происходит со вторым потоком? Он продолжает ждать и будет делать это, пока вновь не освободятся оба объекта-события.

Как я уже упоминал, WaitForMultipleObjects работает на уровне атомарного доступа, и это очень важно. Когда она проверяет состояние объектов ядра, никто не может «у нее за спиной» изменить состояние одного из этих объектов. Благодаря этому исключаются ситуации со взаимной блокировкой. Только представьте, что получится, если один из потоков, обнаружив освобождение hAutoResetEvent1, сбросит его в занятое состояние, а другой поток, узнав об освобождении hAutoResetEvent2, тоже переведет его в занятое состояние. Оба потока просто зависнут: первый будет ждать освобождения объекта, захваченного вторым потоком, а второй — освобождения объекта, захваченного первым. WaitForMultipleObjects гарантирует, что такого не случится никогда.

Тут возникает интересный вопрос. Если несколько потоков ждет один объект ядра, какой из них пробудится при освобождении этого объекта? Официально Майкрософт отвечает на этот вопрос так: «Алгоритм действует честно». Что это за алгоритм, Майкрософт не говорит, потому что не хочет связывать себя обязательствами всегда придерживаться именно этого алгоритма. Она утверждает лишь одно: если объект ожидается несколькими потоками, то всякий раз, когда этот объект переходит в свободное состояние, каждый из них получает шанс на пробуждение.

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

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

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

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

События

События — самая примитивная разновидность объектов ядра. Они содержат счетчик числа пользователей (как и все объекты ядра) и две булевы переменные: одна сообщает тип данного объекта-события, другая — его состояние (свободен или занят).

События просто уведомляют об окончании, какой-либо операции. Объектысобытия бывают двух типов: со сбросом вручную (manual-reset events) и с автосбросом (auto-reset events). Первые позволяют возобновлять выполнение сразу нескольких ждущих потоков, вторые — только одного.

Объекты-события обычно используют в том случае, когда какой-то поток выполняет инициализацию, а затем сигнализирует другому потоку, что тот может продолжить работу. Инициализирующий поток переводит объект «событие» в занятое состояние и приступает к своим операциям. Закончив, он сбрасывает событие в свободное состояние. Тогда другой поток, который ждал перехода события в свободное состояние, пробуждается и вновь становится планируемым.

Объект ядра «событие» создается функцией CreateEvent:

HANDLE CreateEvent(

PSECURITY_ATTRIВUTES psa,

BOOL bManualReset,

BOOL bInitialState,

PCTSTR pszName);

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

Параметр bManualReset (булева переменная) сообщает системе, хотите вы создать событие со сбросом вручную (TRUE) или с автосбросом (FALSE). Параметр bInitialState определяет начальное состояние события — свободное (TRUE) или занятое (FALSE). После того как система создает объект-событие, CreateEvent возвращает описатель события, специфичный для конкретного процесса. В Windows Vista поддерживается новая функция для создания объектов-

событий, CreateEventExr.

HANDLE CreateEventEx(

PSECURITY_ATTRIBUTES psa,

PCTSTR pszName,

DWORD dwFlags,

DWORD dwDesiredAccess);

Ее параметры psa и pszName идентичны одноименным параметрам функции CreateEvent. Параметр dwFlags принимает две битовые маски (см. табл. 9-1).

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

Табл. 9-1. Флаги функции CreateEvent

Константы, определенные в

Описание

WinBase.h

 

CREATE_EVENT_INITIAL_SET

Эквивалент параметра bInitialState, передаваемого функ-

(0x00000002)

ции CreateEvent. Если установлен соответствующий бито-

 

вый флаг, событие инициализируется как свободное, а в

 

противном случае — как занятое.

CREATE_EVENT_MANUAL_RES

Эквивалент параметра bManualReset, передаваемого функ-

ET (6x00000001)

ции CreateEvent. Если установлен соответствующий бито-

 

вый флаг, событие инициализируется как событие с руч-

 

ным сбросом, а в противном случае — как событие с авто-

 

сбросом.

Параметр dwDesiredAccess позволяет во время создания объекта указать уровень доступа для описателя события, возвращаемого функцией. Новая функция может создавать описатели событий с пониженным уровнем доступа, а CreateEvent всегда возвращает описатели с правами полного доступа. Однако более полезно то, что CreateEventEx способна открывать существующие события, Запрашивая при этом пониженный уровень доступа, тогда как CreateEvent всегда запрашивает полный доступ. Например, флаг EVENT_MODIFY_STATE (0x0002) необходим для вызова функций SetEvent, ResetEvent и PulseEvent (о них — чуть позже). Подробнее об этом см. на сайте MSDN (http://msdn2.microsoft.com/enus/library/ms686670.aspx)

Потоки из других процессов могут получать доступ к объектам, вызывая CreateEvent с передачей имени нужного объекта-события в параметре pszName; при помощи наследования; а также вызывая функции DuplicateHandle либо OpenEvent с передачей в параметре pszName имени, указанного при вызове CreateEvent.

HANDLE 0penEvent(

DWORD dwDesiredAccess,

B00L bInherit,

PCTSTR pszName);

Ненужный объект ядра «событие» следует, как всегда, закрыть вызовом

CloseHandle.

Создав событие, вы можете напрямую управлять его состоянием. Чтобы перевести его в свободное состояние, вы вызываете:

B00L S8etEvent(HANDLE hEvent);

А чтобы поменять его на занятое:

B00L ResetEvent(HANDLE hEvent);

Вот так все просто.

Для событий с автосбросом действует следующее правило. Когда его ожидание потоком успешно завершается, этот объект автоматически сбра

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

сывается в занятое состояние. Отсюда и произошло название таких объектовсобытий. Для этого объекта обычно не требуется вызывать ResetEvent, поскольку система сама восстанавливает его состояние. А для событий со сбросом вручную никаких побочных эффектов успешного ожидания не предусмотрено.

Рассмотрим небольшой пример тому, как на практике использовать объекты ядра «событие» для синхронизации потоков. Начнем с такого кода:

// глобальный описатель события со сбросом вручную (в занятом состоянии)

HANDLE g_hEvent;

int WINAPI _tWinMain(…) {

//создаем объект "событие со сбросом вручную" (в занятом состоянии) g_hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);

//порождаем три новых потока

HANDLE hThread[3]; DWORD dwThreadID;

hThread[0] = _beginthreadex(NULL, 0, WordCount, NULL, 0, &dwThreadID); hThread[1] = _beginthreadex(NULL, 0, SpellCheck, NULL, 0, &dwThreadID); hThread[2] = _beginthreadex(NULL, 0, GrammarCheck, NULL, 0, &dwThreadID);

OpenFileAndReadContentsIntoMemory(…);

// разрешаем всем трем потокам обращаться к памяти

SetEvent(g_hEvent);

}

DWORD WINAPI WordCount(PV0ID pvParam) {

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

WaitForSingle0bject(g_hEvent, INFINITE);

//обращаемся к блоку памяти

return(0);

}

DWORD WINAPI SpellCheck (PV0ID pvParam) {

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

WaitForSingleObject(g_hEvent, INFINITE);

//обращаемся к блоку памяти

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

return(0);

}

DWORD WINAPI GrammarCheck (PVOID pvParam) {

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

WaitForSingleObject(g_hEvent, INFINITE);

//обращаемся к блоку памяти

return(0);

}

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

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

Если событие со сбросом вручную заменить событием с автосбросом, программа будет вести себя совершенно иначе. После вызова первичным потоком функции SetEvent система возобновит выполнение только одного из вторичных потоков. Какого именно — сказать заранее нельзя. Остальные два потока продолжат ждать.

Поток, вновь ставший планируемым, получает монопольный доступ к блоку памяти, где хранятся данные, считанные из файла. Давайте перепишем функции потоков так, чтобы перед самым возвратом управления они (подобно функции _tWinMain) вызывали SetEvent. Теперь функции потоков выглядят следующим образом:

DWORD WINAPI WordCount(PVOID pvParam) {

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

WaitForSingleObject(g_hEvent, INFINITE);

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

//обращаемся к блоку памяти

SetEvent(g_hEvent); return(0);

}

DWORD WINAPI SpellCheck (PVOID pvParam) {

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

WaitForSingleObject(g_hEvent, INFINITE);

//обращаемся к блоку памяти

SetEvent(g_h Event); return(0);

}

DWORD WINAPI GrammarCheck (PVOID pvParam) {

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

WaitFprSingleObj ect(g_h Event, INFINITE);

//обращаемся к блоку памяти

SetEvent(g_hEvent); return(0);

}

Закончив свою работу с данными, поток вызывает SetEvent, которая решает системе возобновить выполнение следующего из двух ждущих потоков. И опять мы не знаем, какой поток выберет система, но так или иначе кто-то из них получит монопольный доступ к тому же блоку памяти. Когда и этот поток закончит свою работу, он тоже вызовет SetEvent, после чего с блоком памяти сможет монопольно оперировать третий, последний поток. Обратите внимание, что использование события с автосбросом снимает проблему с доступом вторичных потоков к памяти как для чтения, так и для записи; вам больше не нужно ограничивать их доступ только чтением. Этот пример четко иллюстрирует различия в применении событий со сбросом вручную и с автосбросом.

Для полноты картины упомяну о еще одной функции, которую можно использовать с объектами-событиями:

BOOL PulseEvent(HANDLE hEvent);

PulseEvent освобождает событие и тут же переводит его обратно в занятое состояние; ее вызов равнозначен последовательному вызову SetEvent и ResetEvent. Если вы вызываете PulseEvent для события со сбросом вручную,

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

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

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

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

Эта программа, «09 Handshake.exe» (Файлы исходного кода и ресурсов этой программы находятся в каталоге 09-Handshake в архиве, доступном на веб-сайте поддержки этой книги, http://www.wintellect.com/Books.aspx), демонстрирует применение событий с автосбросом. После запуска Handshake открывается окно, показанное ниже.

Handshake принимает строку запроса, меняет в ней порядок всех символов и показывает результат в поле Result. Самое интересное в программе Handshake — то, как она выполняет эту героическую задачу.

Программа решает типичную проблему программирования. У вас есть клиент

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

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

При запуске программа немедленно создает два объекта-события с автосбросом в занятом состоянии. Один из них, g_hevtRequestSubmitted, используется как индикатор готовности запроса к серверу. Это событие ожидается серверным потоком и освобождается клиентским. Второй объект-событие,

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

g_hevtResultReturned, служит индикатором готовности данных для клиента. Это событие ожидается клиентским потоком, а освобождается серверным.

После создания событий программа порождает серверный поток и выполняет функцию ServerThread. Эта функция немедленно заставляет серверный поток ждать запроса от клиента. Тем временем первичный поток, который одновременно является и клиентским, вызывает функцию DialogBox, отвечающую за отображение пользовательского интерфейса программы. Вы вводите какой-нибудь текст в поле Request и, щелкнув кнопку Submit Request То Server, заставляете программу поместить строку запроса в буфер памяти, разделяемый между клиентским и серверным потоками, а также перевести событие g_hevtRequestSubmitted в свободное состояние. Далее клиентский поток ждет результат от сервера, используя объект-событие g_hevtResultReturned.

Теперь пробуждается серверный поток, обращает строку в блоке разделяемой памяти, освобождает событие g_hevtResultReturned и вновь засыпает, ожидая очередного запроса от клиента. Заметьте, что программа никогда не вызывает ResetEvent, так как в этом нет необходимости: события с автосбросом автоматически восстанавливают свое исходное (занятое) состояние в результате успешного ожидания. Клиентский поток обнаруживает, что событие g_hevtResultReturned освободилось, пробуждается и копирует строку из общего буфера памяти в поле

Result.

Последнее, что заслуживает внимания в этой программе, — то, как она завершается. Вы закрываете ее окно, и это приводит к тому, что DialogBox в функции _tWinMain возвращает управление. Тогда первичный поток копирует в общий буфер специальную строку и пробуждает серверный поток, чтобы тот ее обработал. Далее первичный поток ждет от сервера подтверждения о приеме этого специального запроса и завершения его потока. Серверный поток, получив от клиента специальный запрос, выходит из своего цикла и сразу же завершается. Специальная строка не интерпретируется как команда на завершение, пока отображается главное окно программы, для g_hMainDlg проверяется на NULL-значение.

Я предпочел сделать так, чтобы первичный поток ждал завершения серверного вызовом WaitForMultipleObjects, — просто из желания продемонстрировать, как используется эта функция. На самом деле я мог бы вызвать и WaitForSingleObject, передав ей описатель серверного потока, и все работало бы точно так же.

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

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

Handshake.cpp

/******************************************************************** Module: Handshake.cpp

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

**********************************************A*********************/

#include "..\CommonFiles\CmnHdr.h"

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

#include <windowsx.h>

 

#include <tchar.h>

 

#include "Resource.h"

 

/////////////////////////////////////////////////////////////////////

//это событие освобождается, когда клиент передает запрос серверу

HANDLE g_hevtRequestSubmitted;

//это событие освобождается, когда сервер готов сообщить результат

//клиенту

HAN0LE g_hevtResultReturned;

//это буфер, разделяемый между клиентским и серверным потоками

TCHAR g_szSharedRequestAndResultBuffer[1024];

//Специальное значение, посылаемое клиентом;

//оно заставляет серверный поток корректно завершиться.

TCHAR g_szServerShutdown[] = TEXT("Server Shutdown");

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

//не открыто ли еще главное окно программы.

HWND g_hHainDlg;

/////////////////////////////////////////////////////////////////////

// это код, выполняемый серверным потоком

DWORD WINAPI ServerThread(PV0I0 pvParam) {

// предполагаем, что серверный поток будет выполняться вечно

B00L fShutdown * FALSE;

while (lfShutdown) {

//ждем от клиента передачи запроса

WaitForSingle0bj6Ct(g.hevtRequestSubmltted, INFINITE);

//проверяем, не хочет ли клиент, чтобы сервер завершился fShutdown =

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