Создание эффективных WIN32-приложений с учетом специфики 64-разрядной версии Windows

         

Асинхронный ввод-вывод на устройствах


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

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



Другие функции, применяемые в синхронизации потоков


При синхронизации потоков чащс всего используются функции WaitForSingleObject и WaitForMultipleObjects. OднaкoвWindows ecть и дpyгиe, нecкoлькo oтличaющиecя фyнк ции, которые можно применять с той же целью. Если Вы понимаете, как работают Wait ForSingleObject и WaitForMultipleQbjects, Вы без труда разберетесь и в этих функциях.



Функция MsgWaitForMultipleObjects(Ex)


При вызове MsgWaitForMultipleObjects или MsgWaitForMultipleObjectsEx поток переходит в ожидание своих (предназначенных этому потоку) сообщений:

DWORD MsgWaitForMultipleObjects( DWORD dwCount, PHANDLE phObjects, BOOL fWaitAll, DWORD dwMilliseconds, DWORD dwWakeMask);

DWORD MsgWaitForMultipleObjectsEx( DWORD dwCount, PHANDLE phObjects, DWORD dwMillisGConds, DWORD dwWakeMask DWORD dwFlags);

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

Поток, который создает окна и выполняетдругие операции, относящиеся к пользо вательскому интерфейсу, должен работать с функцией MsgWaitForMultipleObjectsEx, а не с WaitForMultipleObjects, так как последняя не дает возможности реагировать на действия пользователя Подробнее эти функции рассматриваются в главе 26



Функция SignalObjectAndWait


SignalObjectAndWait переводит в свободное состояние один объект ядра и ждет дру гой объеют ядра, выполняя все это как одну операцию на уровне атомарного доступа:

DWORD SignalObjectAndWait( HANDLE hObjectToSignal, HANDLE hObjectToWaitOn, DWORD dwMilliseconds, BOOL fAlertable);

Параметр hObjectToSignal должен идентифицировать мьютекс, семафор или собы тие; объекты любого другого типа заставят SignalObjectAndWait вернуть WAIT_FAILED, а функцию GetLastError — ERROR_INVALIDHANDLE. Функция SignalObjectAndWait про веряет тип объекта и выполняет действия, аналогичные тем, которые предпринима ют функции ReleaseMutex, ReleaseSemaphore (со счетчиком, равным 1) или ResetEvent.



Параметр hObjectToWaitOn идентифицирует любой из следующих объектов ядра: мьютекс, семафор, событие, таймер, процесс, поток, задание, уведомление об изме нении файла или консольный ввод. Параметр dwMilliseconds, как обычно, определяет, сколько времени функция будет ждать освобождения объекта, a флаг fAlertable указы вает, сможет ли поток в процессе ожидания обрабатывать посылаемые ему АРС-вы зовы.

Функция возвращает одно из следующих значений: WAIT_OBJECT_0, WAIT_TIME OUT, WAIT_FAILED, WATT_ABANDONED (см. раздел о мьютексах) или WAIT_IO_COMP LETION.

SignalObjectAndWait — удачное добавление к Windows AFI по двум причинам. Bo псрвых, освобождение одного объекта и ожидание другого — задача весьма распро страненная, а значит, объединение двух операций в одной функции экономит про цессорное время. Каждый вызов функции, заставляющей поток переходить из кода, который работает в пользовательском режиме, в код, работающий в режиме ядра, требует примерно 1000 процессорных тактов (на платформах x86), и поэтому для выполнения, например, такого кода:

ReleaseMutex(hMutex); WaitForSingleObject{hEvent, INFINITE);

понадобится около 2000 тактов. В высокопроизводительных серверных приложени ях SignalObjectAndWait дает заметную экономию процессорного времени.

Во-вторых, без функции SignalObjectAndWait ни у одного потока не было бы воз можности узнать, что другой поток перешел в состояние ожидания. Знание таких вещей очень полеяно для функций типа PulseEvent. Как я уже говорил в этой главе, PulseEvenl переводит событие в свободное состояние и тут же сбрасывает его. Если ни один из потоков не ждет данный объект, событие не зафиксирует этот импульс (pulse). Я встречал программистов, которые пишут вот такой код:


// выполняем какие-то операции

...

SetEvent(hEventWorkerThreadDone);

WaitForSingleObject(hEventMoreWorkToBeDone, INFINITE);
// выполняем еще какие-то операции

...

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

WaitForSingleObject(hEventWorkerTnreadDone); PulseEvent(hEventMoreWorkToBeDone);

Приведенный ранее фрагмент кода рабочего потока порочен по самой своей сути, так как будет работать ненадежно. Ведь вполне вероятно, что после того, как рабо чий поток обратится к SetEvent, немедленно пробудится другой поток и вызовет Pulse Event. Проблема здесь в том, что рабочий поток уже вытеснен и пока еще не получил шанса на возврат из вызова SetEvent, не говоря уж о вызове WaitForSingleObject. В ито

ге рабочий поток не сможет своевременно освободить событие bEventMoreWork ToBeDone

Но если Вы перепишете код рабочего потока с использованием функции Signal ObjectAndWait

// выполняем какие-то операции

SignalObjectAndWait(hEventWorkerThreadDone, hEventMoreWorkToBflDonc, INFINITE, FALSE);
// выполняем еще какие-то операции

то код будет работать надежно, поскольку освобождение и ожидание реализуются на уровне атомарного доступа. И когда пробудичся другой поток, Вы сможете быть аб солютно уверены, что рабочий поток ждет события hEventMoreWorkToBeDone, а зна чит, он обязательно заметит импульс, «приложенный" к событию.

WINDOWS 98
В Windows 98 функция SignalObjectAndWait определена, но не реализована.


Функция WaitForDebugEvent


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

BOOL WaitForDebugEvent( PDEBLIG_F_VENT pde, DWORD dwMimseconds);

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



Функция WaitForlnputldle


Поток может приостановить себя и вызовом WaitForlnputIdle:

DWORD WaitForInputIdle( HANDLE hProcess, DWORD dwMilliseconds);

Эта функция ждет, пока у процесса, идентифицируемого описателем bProcess, не опустеет очередь ввода в потоке, создавшем первое окно приложения. WaitForlnputldle полезна для применения, например, в родительском процессе, который порождает дочерний для выполнения какой-либо нужной ему работы. Когда один из потоков родительского процесса вызывает CreateProcess, он продолжает выполнение и в то время, пока дочерний процесс инициализируется. Этому потоку может понадобить ся описатель окна, создаваемого дочерним процессом. Единственная возможность узнать о моменте окончания инициализации дочернего процесса — дождаться, когда тот прекратит обработку любого ввода Поэтому после вызова CreateProcess поток родительского процесса должен вызвать WaitForInputIdle.

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

WM_KEYDOWN с виртуальной клавишей VK_MENU
WM_KEYDOWN с виртуальной клавишей VK_F
WM_KEYUP с вирчуальной клавишей VK_F
WM_KEYUP с виртуальной клавишей VK_MENU
WM_KEYDOWN с виртуальной клавишей VK_O
WM_KEYUP с виртуальной клавишей VK_O

Эта последовательность дает тот же эффект, что и нажатие клавиш Alt+F, О, — в большинстве англоязычных приложений это вызывает команду Open из меню File. Выбор данной команды открывает диалоговое окно; но, прежде чем оно появится на экране, Windows должна загрузить шаблон диалогового окна из файла и «пройтись» по всем элементам управления в шаблоне, вызывая для каждого из них функцию CreateWindow. Разумеется, на это уходит какое-то время. Поэтому приложение, асин хронно отправившее сообщения типа WM_KEY*, теперь может вызвать WaitForlnput ldle и таким образом перейти в режим ожидания до того момента, как Windows за кончит создание диалогового окна и оно будет готово к приему данных от пользова теля. Далее программа может передать диалоговому окну и сго элементам управле ния сообщения о еще каких-то клавишах, что заставит диалоговое окно проделать те илииныеоперации.

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



И еще кое-что о таймерах


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

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

Конечно, очень мило, что таймеры поддерживают АРС-очереди, но большинство современных приложений использует не APC, а порты завершения ввода-вывода. Как то раз мне понадобилось, чтобы один из потоков в пуле (управляемом через порт завершения ввода-вывода) пробуждался по таймеру через определенные интервалы времени К сожалению, такую функциональность ожидаемые таймеры yе поддержи вают. Для решения этой задачи мнс пришлось создать отдельный поток, который все го-то и делал, что настраивал ожидаемый таймер и ждал его освобождения Когда таймер переходил в свободное состояние, этот поток вызывал PostQueuedComplction Status, передавая соответствующее уведомление потоку в пуле.

Любой, мало-мальски опытный Windows-программист непременно поинтересу ется различиями ожидаемых таймеров и таймеров User (настраиваемых через функ цию SetTimer). Так вот, главное отличие в том, что ожидаемые таймеры реализованы в ядре, а значит, не столь тяжеловесны, как таймеры User. Кроме того, это означает, что ожидаемые таймеры — объекты защищенные.

Таймеры User генерируют сообщения WM_TIMER, посылаемые тому потоку, кото рый вызвал SetTimer (в случае таймеров с обратной связью) или создал определенное


окно (в случае оконных таймеров). Таким образом, о срабатывании таймера User уве домляется только один поток А ожидаемый таймер позволяет ждять любому числу потоков, и, если это таймер со сбросом вручную, при его освобождении может про буждаться сразу несколько потоков.

Если в ответ на срабатывание таймера Вы собираетесь выполнять какие-то опе рации, связанные с пользовательским интерфейсом, то, по-видимому, будет легче структурировать код под таймеры User, поскольку применение ожидаемых таймеров требует от потоков ожидания не только сообщений, но и объектов ядра (Если у Вас есть желание переделать свой код, используйте функцию MsgWaitForMultipleObjects, которая как раз и рассчитана на такие ситуации.) Наконец, в случае ожидаемых тай меров Вы с большей вероятностью будете получать уведомления именно no истече нии заданного интервала. Как поясняется в главе 26, сообщения WM_TIMER всегда имеют наименьший приоритет и принимаются, только когда в очереди потока нет других сообщений Но ожидаемый таймср обрабатывястся так же, как и любой дру гой объект ядра, если он сработал, ждущий поток немедленно пробуждается


Мьютексы


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

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

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

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

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

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

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

HANDLE CreateMutex( PSECURITY_ATTRIBUTES psa, BOOL fIniLialOwner, PCTSTR pszName);

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

HANDLE OpenMutex( DWORD fdwAccess, 800L bInheritHandle, PCTSTR pszName);


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

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

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

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

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

BOOL ReleaseMutex(HANDLE hMutex);

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

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


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


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

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

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

Обьект — критическая секция

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

Малое

Высокое

Возможность использования за границами процесса

Да

Нет

Объявление

HANDLE hmfx;

CRITICAL_SECTION cs;

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

hmtx = CreateMutex (NULL, FALSE, NULL);

InitializeCriticalSection(&cs);

Очистка

CloseHandle(hmtx);

DeleteCriticalSection(&cs);

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

WaitForSingleObject (hmtx, INFINITE);

EnterCrittcalSection(&cs);

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

WaitForSingleObject (hmtx, 0);

TryEnterCriticalSection (&cs);

Ожидание в течение произвольного периода времени

WaitForSingleObject (hmtx, dwMilliseconds);

Невозможно

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

ReleaseMutex(hmtx);

LeaveCriticalSecliun(&cs);

Возможность параллельного ожидания других объектов ядра

Да (с помощью WaitForMultipleObjects или аналогичной функции)

Нет



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


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

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

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



Ожидаемые таймеры


Ожидаемые таймеры (waitahle timers) ~ это объекты ядра, которые самостоятельно переходят в свободное состояние в определенное время или через регулярные про межутки времени. Чтобы создать ожидаемый таймер, достаточно вызвать функцию CreateWaitableTimer.

HANDLE CreateWaitableTimer( PSECURITY_ATTRIBUTES psa, BOOL fManualReset, PCTSTR pszName);

О параметрахр psa и pszName я уже рассказывал в главе 3. Разумеется, любой про цесс может получить свой («процессо-зависимый») описатель существующего объек та "ожидаемый таймер", вызвав OpenWaitableTimer.

HANDLE OpenWaitableTirrer( DWORD dwDesiredAccess, BOOL bInheritHandle, PCTSTR pszName);

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

Объекты «ожидаемый таймер» всегда создаются в занятом состоянии. Чтобы со общить таймеру, в какой момент он должен перейти в свободное состояние, вызови те функцию SetWaitableTimer.

BOOL SetWaitableTimer( HANDLE hTimer, const LARGE_INTEGER *pDueTime, LONG lPeriod, PTIMERAPCROUTINE pfnCompletionRoutine, PVOID pvArgToCotnpletionRoutine, BOOI fResume);

Эта функция принимает несколько параметров, в которых легко запутаться Оче видно, что hTimer определяет нужный таймер. Следующие два параметра (pDиеТiте и lPeriod) используются совместно, первый из них задает, когда таймер должен сра ботать в первый раз, второй определяет, насколько часто это должно происходить в дальнейшем. Попробуем для примера установить таймер так, чтобы в первый раз он сработал 1 января 2002 года в 1:00 PM, а потом срабатывал каждые 6 часов.

// объявляем свои локальные переменные

HANDLE hTimer;
SYSTEMTIME st;
FILETIME ftLocal, ftUTC;
LARGE_INTEGER liUTC;

// создаем таймер с автосбросом
hTimer = CreateWaitableTimer(NULL, FALSE, NULL);

// таймер должен сработать в первый раз 1 января 2002 года в 1:00 PM


// но местному времени

st.wYear = 2002; // год
st.wMonth = 1; // январь
st.wOayOfWeek = 0; // игнорируется
st.wDay = 1, // первое число месяца
st.wHour = 13; // 1 PM
st.wMinute = 0; // 0 минут
st.wSecond = 0, // 0 секунд
st.wMilliseconds = 0; // 0 миллисекунд

SystemTimeToFileTime(&st, &ftLocal);

// преобразуем местное время в UTC-время
LocalFileTimeToFilelime(&ttLocal, &ftUTC);

// преобразуем FILETIME в LARGE_INTEGER из-за различий в выравнивании данных
liUTC.LowPart = ftUTC dwLowDateTime;
liUTC.HighPart = ftUTC dwHighDateTime;

// устанавливаем таймер
SetWaitablcTimer(hTimer, &liUTC, 6 * 60 * 60 * 1000, NULL, NULL, FALSE);

...

Этот фрагмент кода сначала инициализирует структуру SYSTEMTIME, определяя время первого срабатывания таймера (его перехода в свободное состояние). Я уста новил это время как местное. Второй параметр представляется как const LARGE_IN TEGER * и поэтому нс позволяет напрямую использовать структуру SYSTEMTIME. Од нако двоичные форматы структур FILETIME и LARGE_INTEGER идентичны: обе содер жат по два 32-битных значения. Таким образом, мы можем преобразовать структуру SYSTEMTIME в FILETIME. Другая проблема заключается в том, что функция SetWaitable Timer ждет передачи времени в формате UTC (Coordinated Universal Time). Нужное преобразование легко исуществляется вызовом LocalFileTimeToFileTime

Поскольку двоичные форматы структур FILETIMF, и IARGE_INTEGER идентичны, у Вас может появиться искушение передать в SetWaitableTimer адрес структуры FILETIME напрямую;

// устанавливаем таймер
SetWaitableTimer(hTirner, (PLARGE^INTEGER) &ftUTC, 6 * 60 * 60 * 1000, NULL, NULL, FALSE);

В сущности, разбираясь с этой функцией, я так и поступил. По это большая ошиб ка! Хотя двоичные форматы структур FILETIME и LARGE_INTEGER совпадают, вырав нивание этих структур осуществляется по-разному. Адрес любой структуры FILETIME должен начинаться на 32-битной границе, а адрес любой структуры IARGE_INTEGER — на 64-битной. Вызов SetWaitableTimer с передачей ей структуры FILETIME может cpa



ботать корректно, но может и не сработать — все зависит от того, попадет ли начало структуры FlLETIME на 64-битную границу. В то же время компилятор гарантирует, что структура LARGE_INTEGER всегда будет начинаться на 64-битной границе, и по этому правильнее скопировать элементы FILETIME в элементы LARGE_INTEGER, а за тем передать в SetWaitableTtmer адрес именно структуры LARGE_INTEGER.

NOTE:
Процессоры x86 всегда «молча» обрабатываю ссылки на невыровненные дан ные. Поэтому передача в SetWaitableTimer адреса структуры FILETIME будет сра батывать, если приложение выполняется на машине с процессором x86 Од нако другие процессоры (например, Alpha) в таких случаях, как правило, ге нерируют исключение EXCEPTION_DATATYPE_MISALIGNMENT, которое приво дит к завершению Вашего процесса Ошибки, связанные с выравниванием дан ных, — самый серьезный источник проблем при переносе на другие процес сорные платформы программного кода, корректно работавшего на процессо рах x86 Так что, обратив внимание на проблемы выравнивания данных сей час, Вы сэкономите себе месяцы труда при переносе программы на другие платформы в будущем! Подробнее о выравнивании данных см. главу 13.

Чтобы разобраться в том, как заставить таймер срабатывать каждые 6 часов (на чиная с 1:00 PM 1 января 2002 года), рассмотрим параметр lPeriod функции SetWaitable Timer. Этот параметр определяет последующую частоту срабатывания таймера (в мс). Чтобы установить 6 часов, я передаю значение, равное 21 600 000 мс (т e. 6 часов * 60 минут • 60 секунд • 1000 миллисекунд).

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

Следующий код демонстрирует, как установить таймер на первое срабатывание через 5 секунд после вызова SetWaitableTimer.



//объявляем свои локальные переменные
HANDLF hTimer;
LARGE_INTEGER li;

// создаем таймер с автосбросом
hTimer = CreateWaitableTimer(NULL, FALSE, NULL);

// таймер должен сработать через 5 секунд после вызова SetWaitableTimer;
// задаем время в интервалах по 100 нс
const int nTimerUnitsPerSecond = 10000000;

// делаем полученное значение отрицательным, чтобы SetWaitableTimer
// знала: нам нужно относительное, а не абсолютное время li.
QuadPart = -(5 * nTimerUnitsPerSecond);

// устанавливаем таймер (он срабатывает сначала через 5 секунд,
// а потом через каждые 6 часов)
SetWaitableTimer(hTimer, &li, 6 * 60 * 60 * 1000, NULL, NULL, FALSE);

...

Обычно нужно, чтобы таймер сработал только раз — через определенное (абсо лютное или относительное) время перешел в свободное состояние и уже больше никогда не срабатывал Для этого достаточно передать 0 в параметре lPeriod Затем можно либо вызвать CloseHandle, чтобы закрыть таймер, либо перенастроить таймер повторным вызовом SetWattableTimer с другими параметрами

И о последнем параметре функции SetWaitableTimer — lResume. Он полезен на компьютерах с поддержкой режима сна. Обычно в нем передают FALSE, и в приведен ных ранее фрагментах кода я тоже делал так. Но если Вы, скажем, пишете програм му-планировщик, которая позволяет устанавливать таймеры для напоминания о зап ланированных встречах, то должны передавать в этом параметре TRUE Когда таймер сработает, машина выйдет из режима сна (если она находилась в нем), и пробудятся потоки, ожидавшие этот таймер. Далее программа сможет проиграть какой-нибудь WAV-файл и вывести окно с напоминанием о предстоящей встрече. Если же Вы пере дадите FALSE в параметре fResume, объект-таЙмер перейдет в свободное состояние, но ожидавшие его потоки не получат процессорное время, пока компьютер не выйдет из режима сна

Рассмотрение ожидаемых таймеров было бы неполным, пропусти мы функцию CancelWaitable Timer.

BOOL CancelWaitableTimer(HANDLE hTimer);

Эта очень простая функция принимает описатель таймера и отменяет его (тай мер), после чего тот уже никогда не сработает, — если только Вы не переустановите его повторным вызовом SetWaitableTimer. Кстати, если Вам понадобится перенастро ить таймер, то вызывать CancelWattableTimer перед повторным обращением к SetWai tableTimer не требуется; каждый вызов SetWaitableTimer автоматически отменяет пре дыдущие настройки перед установкой новых


Ожидаемые таймеры и АРС-очередь


Теперь Вы знаете, как создавать и настраивать таймер. Вы также научились приоста навливать потоки на таймере, передавая его описатель в WaitForSingleObjects или Wait ForMultipleObjects. Однако у Вас есть возможность создать очередь асинхронных вы зовов процедур (asynchronous procedure call, APC) для потока, вызывающего SetWai tableTimer в момент, когда таймер свободен.

Обычно при обращении к функции SetWaitableTtmer Вы передаете NULL в пара метрах pfnCompletionRoutine и pvArgToCompletionRoutine. В этом случае объект-таймер переходит в свободное состояние в заданное время. Чтобы таймер в этот момент поместил в очередь вызов АРС-функции, нужно реализовать данную функцию и пе редать ее адрес в SetWaitableTimer. АРС-функция должна выглядеть примерно так

VOID APIENTRY TimerAPCRoutine(PVOID pvArgToCompleUonRoutine, DWORD dwTimerLowValue, DWORD dwTimerHighValue)
{

// здесь делаем то, что нужно
}

Я назвал эту функцию TimerAPCRoutine, по Вы можете назвать ее как угодно. Она вызывается из того потока, который обратился к SetWaitableTimer в момент срабаты вания таймера, — но только если вызывающий поток находится в «тревожном» (aler table) состоянии, т. e. ожидает этого в вызове одной из функций SleepEx, WaitForSingle ObjectEx, WaitForMultipleObjectsEx,MsgWaitForMultipleObjectsEx или SignalObjectAndWait Если же поток этого не ожидает в любой из перечисленных функций, система не

поставит в очередь АРС-функцию таймера. Тем самым система не даст АРС-очереди потока переполниться уведомлениями от таймера, которые могли бы впустую израс ходовать колоссальный объем памяти.

Если в момент срабатывания таймера Ваш поток находится в одной из перечис ленных ранее функций, система заставляет его вызвать процедуру обратного вызова Первый ее параметр совпадает с параметром pvArgToCompletionRoutine, передаваемым в функцию SetWaitableTimer, Это позволяет передавать в TimerAPCRoutine какие-либо данные (обычно указатель на определенную Вами структуру) Остальные два парамет ра, dwTimerLowValue и dwTimerHighValue, задают время срабатывания таймера. Код, приведенный ниже, демонстрирует, как принять эту информацию и показать ее поль зователю.


VOID APIENTRY TimerAPCRoutine( PVOID pvArgToCompletionRoutine, DWORD dwTimerLowValue, DWORD dwT:merHighValue)
{

FILETIME ftUTC, ftLocal;
SYSTEMTIME st;
TCHAR szBuf[256];

// записываем время в структуру
FILETIME ftUTC.dwlowDateTime = dwTimerLowValue;
ftUTC.dwHighDateFime = dwTimerHighValue;

// преобразуем UTC-время в местное
FileTimeToLocalFileTime(&ftUTC, &ftLocal);

// преобразуем структуру FILETIME в структуру SYSTEMTIME,
// как того требуют функции GetDateFormat и GetTimeFormat
FileTimetoSystemTime(&ftLocal, &st);

// формируем строку с датой и временем, в которой
// сработал таймер
GetDateFormat(LOCALE_USER_DEFAULT, DATE_LONGDATE, &st, NULL, szBuf, sizeof(szBuf) / sizeof(TCHAR));
_tcscat(szBuf, __TEXT(' '));
GetTimeFormat(LOCALE_USER_DEFAULT, 0, &st, NULL, _tcschr(szBuf, 0), si/eof(szBuf) / sizeor(TCHAR) - _tcslen(sz8uf));

// показываем время пользователю
MessageBox(NULL, szBuf, "Timer went off at ... ", MB_OK); }

Функция «тревожного ожидания" возвращает управление только после обработ ки вссх элементов АРС-очереди. Поэтому Вы должны позаботиться о том, чтобы Ваша функция TimerAPCRoutine заканчивала свою работу до того, как таймер вновь подаeт сигнал (перейдет в свободное состояние). Иначе говоря, элементы не должны ставить ся в АРС-очередь быстрее, чем они могут быть обработаны.

Следующий фрагмент кода показывает, как правильно пользоваться таймерами и APC:

void SomeFunc() {

//создаем таймер (его тип не имеет значения)
HANDLE hTimer = CreateWaitableTimer(NULL, TRUE, NULL);

// настраиваем таймер на срабатывание через 5 секунд
LARGE_INTEGER li = { 0 };

SetWaitableTimer(hTimer, &li, 5000, TimerAPCRoutine, NULL, FALSE);
// ждем срабатывания таймура в "тревожном" состоянии
SleepEx(INFINITE, TRUE);

CloseHandle(hTimer);
}

И последнее. Взгляните ни эют фрагмент кода:

HANDLE hTimer - CreateWaitableTimer(NULL, FAISE, NULL);
SetWaitableTimer(hTimer, ..., TimerAPCRoutine, );
WaitForSingleObjectEx(hTimer, INFINITE, TRUE);

Никогда ие пнигите такой код, потому что вызов WaitForSingleObjectEx на деле за ставляет дважды ожидать таймер — по описателю hTimer и в «тревожном" состоянии Когда таймер перейдет в свободное состояние, поток пробудится, что выведет eго из «тревожного» состояния, и вызова АРС-функции не последует. Правда, АРС-функции редко используются совместно с ожидаемыми таймерами, так как всегда можно дож даться перехода таймера в свободное состояние, а затем сделать то, что нужно.


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


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

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

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

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

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

HANDLE h[2];

h[0] = hAutoResetEvent1;

// изначально занят
h[1] = hAutoResetEvent2;

// изначально занят
WaitForMulUpleObjects(2, И, TRUE, INFINITE);

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


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

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

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

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



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

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


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


Этa программа, «09 Handshakeexe" (см листинг на рис 9-1), демонстрирует приме нение событий с автосбросом. Файлы исходного кода и ресурсов этой программы находятся в каталоге 09-Handshake" на компакт-диске, прилагаемом к книге. После запуска Handshake открывается окно, показанное ниже.

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

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

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

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


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

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

завершения его потока Серверный поток, получив от клиента специальный запрос, выходит из своего цикла и сразу же завершается

Я предпочел сделать так, чтобы первичный поток ждал завершения серверного вызовом WаittForMultipleObjects, - просто из желания продемонстрировать, как исполь зуется эта функция На самом делс я мог бы вызвать и WaitForStngleObject, передав ей описатель серверного потока, и все работало бы точно так же

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

Handshake


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


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

При инициализации Queue создает четыре клиентских и два серверных потока. Каждый клиентский поток засыпает на определенный период времени, а затем поме щает в очередь элемент данных. Когда в очередь ставится новый элемент, содержи мое списка Client Threads обновляется Каждый элемент данных состоит из номера клиентского потока и порядкового номера запроса, выданного этим потоком. Напри мер, первая запись в списке сообщает, что клиентский поток 0 поставил в очередь свой первый запрос. Следующие записи свидетельствуют, что далее свои первые зап росы выдают потоки 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; // количество элементов в массиве
HANDLE m_h[2]; // описатели мьютекса и семафора
HANDLE &m_hmtxQ; // ссылка на m_h[0]
HANDLE &rn_hsemNumElemenls; // ссылка на rc_h[1]

public:
COueue(int nMaxElements);
~CQueue();

BOOL Append(PELtMLNT pElement, DWORD dwMilHseconds);
BOOL Remove(PELEMENT pElement, DWORD dwMilliseconds);
};

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

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

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



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

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

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

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

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

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

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

Queue


Семафоры


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

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

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

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

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

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

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


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

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

Объект ядра «семафор» создается вызовом CreateSemapbore

HANDLE CreateSemaphore( PSECURITY_ATTRIBUTE psa, LONG lInitialCount, LONG lMaximumCount, PCTRTR pszName)

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

HANDLE OpenSemaphore( DWORD fdwAccess, BOOL bInhentHandle, PCTSTR pszName);

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

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

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

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



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

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

BOOL ReleaseSemaphore( HANDLE hSem,

LONG lReleaseCount, PLONG p]PreviousCount);

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

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


самая примитивная разновидность объектов ядра.


События - самая примитивная разновидность объектов ядра. Они содержат счетчик числа пользователей (как и все объекты ядра) и две булевы переменные: одна сооб щает тип данного объекта-события, другая — его состояние (свободен или занят).
События просто уведомляют об окончании какой-либо операции. Объекты-собы тия бывают двух типов: со сбросом вручную (manual-reset events) и с автосбросом (auto-reset events). Первые позволяют возобновлять выполнение сразу нескольких ждущих потоков, вторые — только одного.
Объекты-события обычно используют в том случае, когда какой-то поток выпол няет инициализацию, а затем сигнализирует другому потоку, что тот может продол жить работу. Инициализирующий поток переводит объект "событие» в занятое состо яние и приступает к своим операциям. Закончив, он сбрасывает событие в свободное состояние. Тогда другой поток, который ждал перехода события в свободное состоя ние, пробуждается и вновь становится планируемым.
Объект ядра «событие" создается функцией CreateEvent:
HANDLE CreateEvent(
PSECURITY_ATTRIBUTES psa, BOOL fManualReset, BOOL fInitialState, PCTSTR pszName);
В главе 3 мы обсуждали общие концепции, связанные с объектами ядра, — защи ту, учет числа пользователей объектов, наследование их описателей и совместное использование объектов за счет присвоения им одинаковых имен. Поскольку всс это Вы теперь знаете, я не буду рассматривать первый и последний параметры данной функции.
Пареметр fManualReset (булева переменная) сообщает системе, хотите Вы создать событие со сбросом вручную (TRUE) или с автосбросом (FALSE). Параметру fInitialState определяет начальное состояние события — свободное (TRUE) или занятое (FALSE). После того как система создает объект событие, CreateEvent возвращает описатель события, специфичный для конкретного процесса. Потоки из других процессов мо гут получить доступ к этому объекту: 1) вызовом CreateEvent с тем же параметром pszName;, 2) наследованием описателя; 3) применением функции DuplicateHandle;, и 4) вызовом OpenEvent c передачей в параметре pszName имени, совпадающего с ука занным в аналогичном параметре функции CreateEvent. Вот что представляет собой функция OpenEvent.


HANDLE OpenEvent( DWORD fdwAccess, BOOL fInhent, PCTSTR pszName);
Ненужный объект ядра "событие» следует, как всегда, закрыть вызовом CloseHandle Создав собьпис, Вы можете напрямую управлять его состоянием. Чтобы перевес ти его в свободное состояние, Вы вызываете:
BOOL SetEvenT(HANDLE hEvenеt);
А чтобы поменять его на занятое
BOOL ResetEvent(HANDLE hEvent);
Вот так все просто
Для событий с автосбросом действует следующее правило Когда его ожидание потоком успешно завершается, этот объект автоматически сбрасывается в занятое состояние. Отсюда и произошло название таких объектов-событий Для этого объек та обычно не требуется вызывать ResetEvent, поскольку система сама восстанавливает его состояние А для событий со сбросом вручную никаких побочных эффектов ус пешного ожидания не предусмотрено.
Рассмотрим небольшой пример тому, как на практике использовать объекты ядра «событие» для синхронизации потоков Начнем с такого кода.
// глобальный описатель события со сбросом вручную (в занятом состоянии)
HANDLE g_hEvent;
int WINAPI WinMain( )
{
// создаем объект "событие со сбросом вручную (в занятом состоянии)
g_hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
// порождаем три новых потока
HANDLE hThread[3];
DWORD dwThreadTD;
hThread[0] = _beginthreadex(NULL, 0, WordCount, NULL, 0, &dwThreadlD);
hThread[1] = _beginthreadex(NULL, 0, SpellCheck, NULL, 0, &dwThreadID);
hTbread[2] = _beginthreadex(NULL, 0, GrarrmarCheck, NULL, 0, &dwThreadID);
OpenFileAndReadContentsIntoMemory( );
// разрешаем всем грем потокам обращаться к памяти
SetEvent(g__hEvent),
}
DWORD WINAPI WordCount(PVOID pvParam)
{
// ждем, когда в память будут загружены данные из файла
WaitForSingleObject(g_hEvent, iNeiNITE);
// обращаемся к блоку памяти
return(0);
}
DWORD WINAPI SpellCheck(PVOID pvParam)
{
// ждем, когда в память будут загружены данные из файла
WaitForSingleObject(g_hFvent, INFINITE);
// обращаемся к блоку пэмяти
return(0};
}
DWORD WINAPI GrammarCheck(PVOID pvParam)


{
// ждем, когда в память будут загружены данные из файла
WaitForSingleObject(g_hFvent, INFINITE);
// обращаемся к блоку памяти
return(0);
}
При запуске этот процесс создает занятое событие со сбросом вручную и запи сывает его описатель в глобальную переменную. Это упрощает другим потокам про цесса доступ к тому жс объекту-событию Затем порождается три потока. Они ждут, когда в память будут загружены данные (текст) из некоего файла, и потом обращают ся к этим данным, один поток подсчитывает количество слов, другой проверяет ор фографические ошибки, третий — грамматические Все три функции потоков начи нают работать одинаково каждый поток вызывает WaitForSingleObject, которая при останавливает его до гех пор, пока первичный поток не считает в память содержи мое файла
Загрузив нужные данные, первичный поток вызывает SetEvent, которая переводит событие в свободное состояние. В этот момент система пробуждает три вторичных потока, и они, вновь получив процессорное время, обращаются к блоку памяти За метьте, что они получают доступ к памяти в режиме только для чтения. Это единствен ная причина, по которой все три потока могут выполняться одновременно
Если событие со сбросом вручную заменить на событие с автосбросом, програм ма будет вести себя совершенно иначе После вызова первичным потоком функции SetEvent система возобновит выполнение только одного из вторичных потоков. Ка кого именно — сказать заранее нельзя. Остальные два потока продолжат ждать.
Поток, вновь ставший планируемым, получает монопольный доступ к блоку па мяги, где хранятся данные, считанные из файла Давайте перепишем функции пото ков так, чтобы перед самым возвратом управления они (подобно функции WinMain) вызывали SetFvent Теперь функции потоков выглядят следующим образом:
DWORD WINAPI WordCount(PVOID pvParam)
{
// ждем, когда в память будут загружены данные из файла
WaitForSingleObject(a_hEvent, INFINITE);
// обращаемся к блоку памяти
SetEvent(g_hEvent);
return(0);
}
DWORD WINAPI SpellCneck(PVOID pvParam)


{
// ждем, когда в память будут загружены данные из файла
WaitForSingleObject(g htvent, INFINITE);
// обращаемся к блоку памяти
SetEvent(g_hEvent);
return(0);
}
DWORD WINAPI GrammarCheck(PVOID pvParam)
{
// ждем, когда в память будут загружены данные из файла
WaitForSingleObject(g_hEvent, INFINITF);
// обращаемся к блоку памяти
SetEvent(g_hEvent);
return(0);
}
Закончив свою работу с данными, поток вызывает SetEvent, которая разрешает системе возобновить выполнение следующего из двух ждущих потоков. И опять мы не знаем, какой поток выберет система, но так или иначе кто-то из них получит мо нопольный доступ к тому же блоку памяти. Когда и этот поток закончит свою работу, он тоже вызовет SetEvent, после чего с блоком памяти сможет монопольно опериро вать третий, последний поток Обратите внимание, что использование события с ав тосбросом снимает проблему с доступом вторичных потоков к памяти как для чте ния, так и для записи; Вам больше не нужно ограничивать их доступ только чтением. Этот пример четко иллюстрирует различия в применении событий со сбросом вруч ную и с автосбросом.
Для полноты картины упомяну о еще одной функции, которую можно использо вать с объектами-событиями
BOOL PulseEvent(HANDLE hEvent);
PuteeEvent освобождает событие и тут жс переводит его обратно в занятое состо яние; ее вызов равнозначен последовательному вызову SelEvent и ResetEvent. Если Вы вызываете PulseEvent для события со сбросом вручную, любые потоки, ждущие этот объект; становятся планируемыми. При вызове этой функции применительно к со бытию с автосбросом пробуждается только одип из ждущих потоков. А если ни один из потоков не ждет объект-событие, вызов функции не дает никакого эффекта
Особой пользы от PulseEuent я не вижу В сущности, я никогда не пользовался ею на практике, потому что абсолютно неясно, какой из потоков заметит этот импульс и станет планируемым Наверное, в каких-то сценариях PulseEuent можст пригодиться, но ничего такого мне в голову не приходит Когда мы перейдем к рассмотрению фун кции SignalObjectAndWait, я расскажу о PulseEvent чуть подробнее.

Сводная таблица объектов, используемых для синхронизации потоков


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

Объект

Находится в занятом состоянии, когда

Переходит в свободное состояние, когда

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

Процесс
Поток

процесс еще активен поток еще активен

процесс завершается (ExitProcess, TerminateProcess)
поток завершается (ExitThread, TerminateThread)

Нет
Нет

Объект

Находится в занятом состоянии, когда:

Переходит в свободное состояние, когда:

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

Задание

время, выделенное заданию, еще не истекло

время, выделенное заданию, истекло

Нет

Файл

выдан запрос на ввод-вывод

завершено выполнение запроса на ввод-вывод

Нет

Консольный ВВОД

ввода нет

ввод есть

Нет

Уведомление об изменении файла

в файловой системе нет изменений

файловая система обнаруживает изменения

Сбрасывается в исходное состояние

Событие с автосбросом

вызывается ResetEvent, PulseEvent или ожидание успешно завершилось

вызывается SetEvent или PulseEvent

Сбрасывается в исходное состояние

Событие со сбросом вручную

вызывается ResetEvent или PulseEvent

вызывается SetEvent или PulseEvent

Нет

Ожидаемый таймер с автосбросом

вызывается CancelWaitable- Тiтеr или ожидание успешно завершилось

наступает время срабатывания (SetWaitableTimer)

Сбрасывается в исходное состояние

Ожидаемый таймер со сбросом вручную

вызывается CancelWaitableTimer

наступает время срабатывания (SetWaitableTimef)

Нет

Семафор

ожидание успешно завершилось

счетчик > 0 (ReleaseSemaphore)

Счетчик уменьшается на 1

Мьютекс

ожидание успешно завершилось

поток освобождает мьютекс (ReleaseMutex)

Передается пото ку во владение

Критическая секция (поль зовательского режима)

ожидание успешно завершилось ( (Try)EnterCriticalSection)

поток освобождает критическую секцию (LeaveCriticalSection)

Передается потоку во владение

Interlocked-функции (пользовательского режима) никогда не приводят к исключе нию потока из числа планируемых; они лишь изменяют какое-то значение и тут же возвращают управление.



Wait-функции


Wait-функции позволяют потоку в любой момент приостановиться и ждать освобож дения какого-либо объекта ядра. Из всего семейства этих функций чаще всего исполь зуется WaitForSingleObject:

DWORD WaitForSingleObject( HANDLE hObject, DWORD dwMilliseconds);

Когда поток вызывает эту функцию, первый параметр, hObject, идентифицирует объект ядра, поддерживающий состояния «свободен-занят» (То есть любой объект, упомянутый в списке из предыдущего раздела.) Второй параметр, dwMilliseconds, ука зывает, сколько времени (в миллисекундах) поток готов ждать освобождения объекта.

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

WaitForSingleObject(hProcess, INFINITE);

В данном случае константа INFINITE, передаваемая во втором параметре, подска зывает системе, что вызывающий поток готов ждать этого события хоть целую веч ность. Именно эта коистанта обычно и передается функции WaitForSingleObject, но Вы можете указать любое значение в миллисекундах. Кстати, константа INFINITE опре делена как 0xFFFFFFFF (или -1). Разумеется, передача INFINlTE нс всегда безопасна Если объект так и не перейдет в свободное состояние, вызывающий поток никогда не проснется; одно утешение, тратить драгоценное процессорное время он при этом не будет

Вот пример, иллюстрирующий, как вызывать WaitForSingleObject co значением тай маута, отличным от INFINITE

DWORD dw = WaitForSlngleObject(hProcess, 5000);

switch (dw)
{
case WAIT_OBJECT_0:
// процесс завершается
break;

case WAIT_TIMEOUT:
// процесс не завершился в течение 5000 мс
break;

case WAIT_FAILED:
// неправильный вызов функции (неверный описатель?)
break;
}

Данный код сообщает системе, что вызывающий поток не должен получать про цессорное время, пока не завершится указанный процесс или не пройдет 5000 мс (в зависимости от того, что случится раньше). Поэтому функция вернет управление либо до истечения 5000 мс, если процесс завершится, либо примерно через 5000 мс, если процесс к тому времени не закончит свою работу Заметьте, что в параметре dwMilli seconds можно передать 0, и гогда WaitForSingleObject немедленно вернет управление


Возвращаемое значение функции WaitForSingleObject указывает, почему вызываю щий поток снова стал планируемым Если функция возвращает WAITOBTECT_0, объ ект свободен, а если WAIT_TIMEOUT — заданное время ожидания (таймаут) истекло. При передаче неверного параметра (например, недопустимого описателя) WaitForSing leObject возвращает WAIT_ EAILED. Чтобы выяснить конкретную причину ошибки, вы зовите функцию GetLastErroY.

Функция WaitForMultipleObjects аналогична WaitForSingleObject c тем исключением, что позволяет ждать освобождения сразу нескольких объектов или какого-то одного из списка объектов:

DWORD WaitForMultipleObjects( DWOHD dwCount, CONST HANDLE* phObjects, BOOL fWaitAll, DWORD dwMilliseconds);

Параметр dwCount определяет количество интересующих Вас объектов ядра Его значениедолжло быть в пределах от 1 до MAXIMUM_WAIT_OBJECTS (в заголовочных файлах Windows оно определено как 64). Параметр phObject — это указатель на мас сив описателей объектов ядра.

WaitForMultipleObjects приостанавливает поток и засгавляет его ждать освобожде ния либо всех заданных объектов ядра, либо одного из них. Параметр fWaitAll как раз и определяет, чего именно Вы хотите от функции. Если он равен TRUE, функция не даст потоку возобновить свою работу, пока нс освободятся все объекты.

Параметр dwMilliseconds идентичен одноименному параметру функции WaitFor SingleObject Если Вы указываете конкретное время ожидания, то no его истечении функция в любом случае возвращает управление. И опять же, в этом параметре обыч но передают INFINITE (будьте внимательны при написании кода, чтобы не создать ситуацию взаимной блокировки).

Возвращаемое значение функции WaitForMultipleObjects сообщает, почему возоб новилосъ выполнение вызвавшего ее потока Значения WAIT_FAILED и WAIT_TIMEOUT никаких пояснений не требуют. Если Вы передали TRUE в параметре fWaitAll и всс объекты перешли в свободное состояние, функция возвращает значение WAIT_OB JECT_0. Если fWaitAll приравнен FALSE, она возвращает управление, как только ос вобождается любой из объектов. Вы, по-видимому, захотите выяснить, кякой именно объект освободился В этом случае возвращается значение от WAIT_OBJECT_0 до WAIT_OBJECT_0 + dwCount - 1. Иначе говоря, если возвращаемое значение не равно WAIT_TIMEOUT или WAIT_FAILED, вычтите из него значение WAlT_OBJECT_0, и Вы получите индекс в массиве описателей, на который указывает второй параметр фун кции WaitForMultipleObjects. Индекс подскажет Вам, какой объект перешел в незаня тое состояние. Поясню сказанное на примере.



HANDLE h[3];
h[0] = hProcess1;
h[1] = hProcess2;
h[2] = hProcess3,

DWORD dw = WaitForMultipleObjects(3, h, FALSE, 5000);

switch (dw)
{

case WAIT_FAILED:
// неправильный вызов функции (неверный описатель?)
break;

case WAIT_TIMEOUT:
// ни один из объектов не освободился в течение 5000 мс
break;

case WAIT_OBJECTJ) + 0:
// завершился процесс, идентифицируемый h[0], т e описателем (hProcess1)
break;

case WATT_OBJECT_0 + 1:
// завершился процесс, идентифицируемый h[1], т e описателем (hProcess2)
break;

case WAIT_OBJECT_0 + 2:
// завершился процесс, идентифицируемый h[2], т. e описателем (hProcess3)
break;
}

Если Вы передаете FALSE в параметре fWaitAll, функция WaitForMultipleObjects ска нирует массив описателей (начиная с нулевого элемента), и первый же освободив шийся объект прерывает ожидание Это может привести к нежелательным последстви ям. Например, Ваш поток ждет завершения трех дочерних процессов; при этом Вы передали функции массив с их описателями. Если завершается процесс, описатель которого находится в нулевом элементе массива, WaitForMultipleObjects возвращает управление. Теперь поток может сделать то, что ему нужно, и вновь вызвать эту фун кцию, ожидая завершения другого процесса. Если поток передаст те же три описате ля, функция немедленно всрнст управление, и Вы снова получите значение WAIT_OB JECT_0. Таким образом, пока Вы не удалите описатели тех объектов, об освобожде нии которых функция уже сообщила Вам, код будет работать некорректно.