лекции / Shchupak_Yu._Win32_API_Razrabotka_prilozheniy_dlya_Windows
.pdfСинхронизация |
461 |
|
|
Если критический участок не используется в данный момент другим потоком, он обозначается системой как занятый, и поток немедленно продолжает выпол" нение. Если критический участок уже используется, то поток блокируется до тех пор, пока участок не будет освобожден.
После вызова EnterCriticalSection следуют инструкции, принадлежащие крити" ческому участку.
Конец критического участка обозначается вызовом функции LeaveCriticalSection:
LeaveCriticalSection(&cs);
Как только поток получает контроль над критическим участком, доступ дру" гих потоков к этому участку блокируется. При этом очень важно, чтобы время выполнения критического участка было минимальным. Это позволит добиться наилучших результатов работы приложения.
Если критический участок больше не нужен, используемые им ресурсы осво" бождаются вызовом функции DeleteCriticalSection.
Кроме перечисленных функций Windows NT/2000 предоставляет функцию TryEnterCriticalSection, вызывая которую, поток пытается захватить критический участок:
BOOL bAcquired = TryEnterCriticalSection(&cs); if (bAcquired) {
// Запись в защищенные переменные
. . .
}
else {
// Контроль над критическим участком недопустим
. . .
}
Если критический участок доступен, то поток становится его «владельцем» и осуществляет запись в защищенные переменные. Если участок недоступен, то поток не блокируется, как в случае применения функции EnterCriticalSection, а за" нимается другой работой, за что отвечает ветвь else.
Работая с критическими секциями или применяя Interlocked"функции, програм" ма не переключается в режим ядра. Поэтому эти виды синхронизации называют
синхронизацией в пользовательском режиме. Она отличается высокой скоростью реализации. Главным недостатком такой синхронизации является невозможность ее применения для потоков, принадлежащих разным процессам.
Описываемые ниже примитивы синхронизации основаны на использовании объектов ядра. Но сначала мы должны рассмотреть так называемые wait"функции.
Wait-функции
Многие объекты ядра могут находиться либо в свободном (signaled state), либо
взанятом состоянии (nonsignaled state). К таким объектам относятся:
процессы;
потоки;
задания;
файлы;
консольный ввод;
462 |
Глава 9. Многозадачность |
|
|
уведомления об изменении файлов;
события;
ожидаемые таймеры;
семафоры;
мьютексы.
Wait"функции позволяют потоку в любой момент приостановиться и ждать освобождения какого"либо объекта ядра. Важным свойством функций этого се" мейства является то, что они не тратят процессорное время, пока ждут освобож" дения объекта или наступления тайм"аута.
Функция WaitForSingleObject
Функция имеет следующий прототип:
DWORD WaitForSingleObject(HANDLE hHandle, DWORD dwMillisecond);
Когда поток вызывает эту функцию, параметр hHandle идентифицирует объект ядра, поддерживающий состояния «свободен–занят». Параметр dwMillisecond за" дает тайм"аут (time out) — интервал времени, спустя которое функция возвраща" ет управление, даже если объект остается в занятом состоянии. Если параметр dwMillisecond имеет нулевое значение, то функция только проверяет состояние объекта и возвращает управление немедленно. Константа INFINITE в качестве зна" чения dwMillisecond задает бесконечное значение тайм"аута.
Возвращаемым значением функции чаще всего является одна из следующих констант:
Константа |
Интерпретация |
|
|
WAIT_OBJECT_0 |
Контролируемый объект ядра перешел в свободное состояние |
WAIT_TIMEOUT |
Истек интервал тайм-аута, а контролируемый объект ядра остается |
|
в занятом состоянии |
WAIT_FAILED |
Функция завершилась с ошибкой. Для получения дополнительной |
|
информации об ошибке нужно использовать функцию GetLastError |
|
|
Вот пример обработки кода возврата функции WaitForSingleObject:
DWORD dw = WaitForSingleObject(hProcess, 5000); switch (dw) {
case WAIT_OBJECT_0:
// код обработки для завершившегося процесса break;
case WAIT_TIMEOUT:
// код обработки, если процесс не завершился в течение 5000 мс break;
case WAIT_FAILED:
// функция завершилась с ошибкой. break;
}
Будьте осторожны, используя константу INFINITE в качестве второго параметра функции WaitForSingleObject. Если объект hHandle так и не перейдет в свободное со" стояние, то вызывающий поток никогда не проснется. Единственное «утешение» зак" лючается в том, что тратить драгоценное процессорное время он при этом не будет.
Синхронизация |
463 |
|
|
Функция WaitForMultipleObjects
Функция имеет следующий прототип:
DWORD WaitForMultipleObjects(DWORD nCount, CONST HANDLE* lpHandles,
BOOL fWaitAll, DWORD dwMilliseconds);
Она работает так же, как и функция WaitForSingleObject, но при этом позволяет ждать освобождения сразу нескольких объектов или какого"то одного объекта из заданного списка.
Параметр nCount определяет количество интересующих вас объектов ядра. Его значение должно быть в пределах от 1 до MAXIMUM_WAIT_OBJECTS. В заголовочных файлах Windows эта константа имеет значение 64. Параметр lpHandles содержит указатель на массив дескрипторов объектов ядра. В массиве могут содержаться дескрипторы объектов разных типов.
Функция WaitForMultipleObjects приостанавливает поток и заставляет его ждать ос" вобождения либо всех заданных объектов ядра, либо одного из них. Параметр fWaitAll определяет поведение функции. Значение TRUE задает режим ожидания освобожде" ния всех указанных объектов, а FALSE — только одного из них. В последнем случае код возврата функции содержит информацию о том, какой именно объект освободился.
Возвращаемое функцией значение сообщает, почему возобновилось выполне" ние вызвавшего ее потока. Значения WAIT_TIMEOUT и WAIT_FAILED интерпретиру" ются по аналогии с функцией WaitForSingleObject. Если параметр fWaitAll равен TRUE и все объекты перешли в свободное состояние, то функция возвращает значение WAIT_OBJECT_0. Если же fWaitAll имеет значение FALSE, то функция возвращает уп" равление, как только освобождается любой из объектов. При этом ее код возврата лежит в интервале от WAIT_OBJECT_0 до WAIT_OBJECT_0 + nCount – 1. Иными слова" ми, если из кода возврата вычесть константу WAIT_OBJECT_0, то вы получите ин" декс освободившегося объекта в массиве lpHandles.
Вот пример обработки кода возврата функции WaitForMultipleObjects:
HANDLE hp[3]; hp[0] = hProcess0; hp[1] = hProcess1; hp[2] = hProcess2;
DWORD dw = WaitForMultipleObjects (3, hp, FALSE, 4000); switch (dw) {
case WAIT_OBJECT_0 + 0:
// завершился процесс с дескриптором hp[0] break;
case WAIT_OBJECT_0 + 1:
// завершился процесс с дескриптором hp[1] break;
case WAIT_OBJECT_0 + 2:
// завершился процесс с дескриптором hp[2] break;
case WAIT_TIMEOUT:
// ни один из объектов не освободился в течение 4000 мс break;
case WAIT_FAILED:
// функция завершилась с ошибкой. break;
}
Синхронизация |
465 |
|
|
уже создал ранее объект"событие с таким же именем EventName. В этом случае система проверяет права доступа процесса A к этому объекту. Если с правами до" ступа все в порядке, то в таблице дескрипторов процесса A создается новая запись с дескриптором hEvent. То есть процесс A получает свой дескриптор уже суще" ствующего именованного объекта"события, а счетчик пользователей этого объек" та увеличивается на единицу.
Предположим, что имеет место другая ситуация, когда некий процесс B уже создал ранее объект ядра с таким же именем, но другого типа, например семафор. Тогда функция CreateEvent вернет значение NULL, а если после этого вызвать функ" цию GetLastError, то она вернет код ошибки, равный 6 (ERROR_INVALID_HANDLE).
Наконец, самая простая ситуация — когда объект ядра с таким именем (EventName) в момент вызова функции CreateEvent не существует. Тогда действительно будет создан новый объект ядра «событие» с именем EventName.
Так что будьте аккуратны с именованием объектов ядра и учитывайте, что про странство имен объектов ядра является общим для объектов всех типов.
Объекты"события могут разделяться разными процессами. Допустим, что не" кий процесс A создал новое событие hEvent с именем EventName. Потоки из других процессов могут получить доступ к этому объекту несколькими способами:
вызовом функции CreateEvent с передачей в параметре pszName такого же име" ни (эта ситуация только что рассматривалась);
наследованием дескриптора;
применением функции DuplicateHandle;
вызовом функции OpenEvent с передачей в параметре pszName такого же имени.
Функция OpenEvent имеет следующий прототип:
HANDLE OpenEvent( |
|
|
DWORD dwDesiredAccess, // |
требуемый доступ |
|
BOOL bInheritHandle, |
// опция наследования |
|
LPCTSTR pszName |
// |
имя объекта |
); |
|
|
Параметр dwDesiredAccess определяет требуемый доступ к объекту"событию. Его значением может быть любая комбинация следующих флагов:
Флаг доступа |
Описание |
|
|
EVENT_ALL_ACCESS |
Все возможные флаги доступа к объекту-событию |
EVENT_MODIFY_STATE |
Разрешено использование дескриптора объекта в функциях SetEvent |
|
è ResetEvent |
SYNCHRONIZE |
Разрешено использование дескриптора объекта в любых wait-функциях |
|
|
Функция OpenEvent завершится с ошибкой, если атрибуты доступа, заданные для объекта с именем pszName при вызове функции CreateEvent, не будут разре" шать доступ, указанный параметром dwDesiredAccess.
Параметр bInheritHandle определяет, разрешено ли наследование данного объек" та при создании дочерних процессов. Последний параметр pszName указывает на имя существующего объекта"события.
Если функция завершается успешно, то возвращаемое значение содержит дес" криптор существующего объекта"события. Если возникла ошибка, то функция возвращает значение NULL. При помощи функции GetLastError можно уточнить при" чину возникновения ошибки.
466 |
Глава 9. Многозадачность |
|
|
Создав событие или получив дескриптор существующего события, вы можете управлять его состоянием с помощью функции
BOOL SetEvent(HANDLE hEvent);
которая переводит объект hEvent в свободное состояние, или с помощью функ" ции
BOOL ResetEvent(HANDLE hEvent);
которая переводит его в занятое состояние.
Для событий с автосбросом действует следующее правило. Когда ожидание потоком освобождения события успешно завершается, этот объект автоматичес" ки сбрасывается в занятое состояние. Для событий со сбросом вручную автома" тического сбрасывания не происходит, поэтому для возврата в занятое состояние необходимо вызвать функцию ResetEvent.
Пример использования объектов"событий приводится в листингах 9.4 и 9.5.
Семафоры
Объекты ядра «семафор» (semaphore) используются для учета ресурсов. Кроме счетчика числа пользователей семафор содержит два 32"битных значения со зна" ком. Одно из них определяет максимальное количество ресурсов, контролируе" мых семафором, а другое используется как счетчик текущего числа ресурсов.
Объект ядра «семафор» создается вызовом функции CreateSemaphore:
HANDLE CreateSemaphore(
LPSECURITY_ATTRIBUTES semaphoreAttributes, // атрибуты доступа
LONG lInitialCount, |
// текущее количество доступных ресурсов |
LONG lMaximumCount, |
// максимальное количество ресурсов |
LPCTSTR pszName |
// имя объекта |
); |
|
Например, в результате следующего вызова:
HANDLE hSem = CreateSemaphore(NULL, 0, 5, "MySemaphore");
будет создан именованный объект"семафор с максимальным числом ресурсов, равным пяти, и изначально нулевым количеством доступных ресурсов.
Поток может увеличить значение счетчика текущего числа доступных ресур" сов на величину lReleaseCount, вызывая функцию ReleaseSemaphore:
BOOL ReleaseSemaphore( |
|
|
|
|
HANDLE hSemaphore, |
// |
дескриптор |
семафора |
|
LONG lReleaseCount, |
// приращение количества доступных ресурсов |
|||
LPLONG lpPreviousCount // |
предыдущее |
значение |
счетчика ресурсов |
|
); |
|
|
|
|
По адресу, указанному в последнем параметре, функция записывает предыду" щее значение счетчика ресурсов. Если оно вас не интересует, то просто передайте в параметре lpPreviousCount значение NULL.
Как обычно, любой процесс может получить свой («процессозависимый») дес" криптор существующего объекта «семафор», вызвав функцию OpenSemaphore:
HANDLE OpenSemaphore ( |
|
|
DWORD dwDesiredAccess, // |
требуемый доступ |
|
BOOL bInheritHandle, |
// параметр наследования |
|
LPCTSTR pszName |
// |
имя объекта |
); |
|
|
Синхронизация |
467 |
|
|
Параметр dwDesiredAccess определяет требуемый уровень доступа к объекту" семафору. Его значением может быть любая комбинация следующих флагов:
Флаг доступа |
Описание |
|
|
SEMAPHORE_ALL_ACCESS |
Все возможные флаги доступа к объекту-семафору |
SEMAPHORE _MODIFY_STATE |
Разрешено использование дескриптора объекта в функции |
|
ReleaseSemaphore |
SYNCHRONIZE |
Разрешено использование дескриптора объекта в любых wait- |
|
функциях |
|
|
Если функция завершается успешно, то возвращаемое значение содержит дес" криптор существующего объекта"семафора. Если возникла ошибка, функция воз" вращает значение NULL. При помощи функции GetLastError можно уточнить при" чину возникновения ошибки.
Поток получает доступ к ресурсу, вызывая одну из wait"функций и передавая ей дескриптор семафора, охраняющего этот ресурс. Wait"функция проверяет у семафора значение счетчика текущего числа ресурсов. Если оно больше нуля (то есть семафор свободен), то значение счетчика уменьшается на единицу, а вы" зывающий поток продолжает выполнение. Очень важно, что эта операция вы" полняется для семафора на уровне атомарного доступа. Иначе говоря, пока wait" функция не вернет управление, операционная система не позволит прервать эту операцию никакому другому потоку.
Если wait"функция определяет, что счетчик текущего числа ресурсов равен нулю (семафор занят), то система переводит вызывающий поток в состояние ожи" дания. Когда другой поток увеличит значение этого счетчика, система вспомнит о ждущем потоке и снова начнет выделять ему процессорное время. А он, захва" тив ресурс, уменьшит значение счетчика на единицу.
Мьютексы
Объекты ядра «мьютексы» (mutex) гарантируют потокам взаимоисключающий доступ к единственному ресурсу. Кроме счетчика пользователей мьютекс содер" жит счетчик рекурсии и переменную, в которой запоминается идентификатор потока"владельца. Мьютексы ведут себя подобно критическим секциям. Однако если критические секции являются объектами пользовательского режима, то мью" тексы — это объекты ядра. Поэтому они позволяют синхронизировать доступ к ресурсу нескольких потоков из разных процессов.
Для использования объекта"мьютекса один из процессов должен сначала соз" дать его вызовом функции CreateMutex:
HANDLE CreateMutex (
LPSECURITY_ATTRIBUTES mutexAttributes, // атрибуты доступа
BOOL bInitialOwner, |
// флаг наличия потока-владельца |
LPCTSTR pszName |
// имя объекта |
); |
|
Параметр bInitialOwner определяет начальное состояние мьютекса. Если он име" ет значение FALSE, то объект"мьютекс не принадлежит ни одному из потоков и поэтому находится в свободном состоянии. При этом его идентификатор пото" ка"владельца и счетчик рекурсии равны нулю. Если же в этом параметре переда" ется значение TRUE, то идентификатор потока"владельца в мьютексе приравнива"
468 |
Глава 9. Многозадачность |
|
|
ется идентификатору вызывающего потока, а счетчик рекурсии получает единич" ное значение. Если идентификатор потока"владельца не равен нулю, мьютекс счи" тается занятым.
Разумеется, любой процесс может получить свой («процессозависимый») дес" криптор существующего объекта «мьютекс», вызвав функцию OpenMutex:
HANDLE OpenMutex(DWORD dwDesiredAccess, BOOL bInheritHandle,
LPCTSTR pszName);
Значением параметра dwDesiredAccess может быть любая комбинация флагов:
Флаг доступа |
Описание |
|
|
MUTEX_ALL_ACCESS |
Все возможные флаги доступа к объекту-мьютексу |
SYNCHRONIZE |
Разрешено использование дескриптора объекта в любых wait-функциях, |
|
а также при вызове ReleaseMutex |
|
|
Если функция завершается успешно, то возвращаемое значение содержит дес" криптор существующего объекта"мьютекса. В случае возникновения ошибки функ" ция возвращает значение NULL.
Для получения доступа к разделяемому ресурсу поток вызывает одну из wait" функций и передает ей дескриптор мьютекса, охраняющего этот ресурс. Wait"фун" кция проверяет у мьютекса идентификатор потока"владельца. Если идентифика" тор равен нулю, то ресурс свободен и вызывающий поток может продолжить выполнение. В этом случае перед возвратом из wait"функции идентификатор по" тока"владельца в мьютексе становится равным идентификатору вызывающего потока, а счетчику рекурсии присваивается единичное значение.
Если wait"функция определяет, что идентификатор потока"владельца не ра" вен нулю, то вызывающий поток переходит в состояние ожидания. Система запо" минает это и, когда идентификатор потока"владельца обнуляется (а перед этим обнуляется и счетчик рекурсии), записывает в него идентификатор ожидающего потока, а счетчику рекурсии присваивает единичное значение. После этого ожи" дающий поток вновь становится планируемым. Все проверки и изменения состо" яния объекта"мьютекса выполняются на уровне атомарного доступа.
Как только разделяемый ресурс перестает быть нужным, поток, владеющий мьютексом, должен его освободить, вызвав функцию ReleaseMutex. Функция ReleaseMutex уменьшает значение счетчика рекурсии в мьютексе на единицу. Если счетчик рекурсии становится равным нулю, то обнуляется также идентификатор потока"владельца.
Система допускает рекурсивный многократный захват мьютекса одним пото" ком. Это происходит, если, уже владея мьютексом и не освободив его, поток вы" зывает wait"функцию с дескриптором этого же мьютекса. В этом случае счетчик рекурсии в мьютексе каждый раз увеличивается на единицу. Для освобождения мьютекса количество вызовов потоком функции ReleaseMutex должно совпадать
сколичеством предыдущих захватов мьютекса.
Ачто будет, если поток, владеющий мьютексом, завершится, не успев его осво" бодить? В таком случае система считает, что произошел отказ от мьютекса, и автоматически переводит его в свободное состояние. Если этот мьютекс ждут другие потоки, то система выбирает один из них и позволяет ему захватить мью" текс. Тогда wait"функция возвращает потоку значение WAIT_ABANDONED вместо WAIT_OBJECT_0. Это позволяет понять, что мьютекс освобожден некорректно.
Обмен данными между процессами |
469 |
|
|
Обычно программы после возврата из wait"функции специально не проверя" ют возвращаемое значение на WAIT_ABANDONED, поскольку такое завершение по" токов происходит очень редко. Но на стадии отладки приложения может оказать" ся полезным обнаружение ситуаций с некорректным освобождением мьютексов.
Обмен данными между процессами
Потоки одного процесса не имеют доступа к адресному пространству другого про" цесса. Однако существуют специализированные механизмы для передачи данных между процессами. Обмен данными между процессами (Interprocess Communication, IPC) имеет долгую историю развития вместе с развитием вычис" лительной техники. Последним впечатляющим примером технологической эво" люции в области обмена информацией является создание всемирной паутины (World Wide Web).
Приведем краткий список механизмов IPC, встроенных в Windows:
Буфер обмена (clipboard) — одна из самых примитивных и хорошо известных форм IPC. Основная его задача состоит в поддержке обмена данными между программами по желанию и под контролем пользователя.
Библиотеки динамической компоновки. Когда в рамках DLL объявляется пе" ременная, то ее можно сделать разделяемой (shared). Все процессы из DLL, обращающиеся к такой переменной, будут использовать одно и то же место в физической памяти.
Сообщение WM_COPYDATA, которое применяется для передачи участка памяти другому процессу.
Разделяемая память (shared memory) реализуется при помощи объектов ядра «проекция файла». Этот механизм основан на отображении файлов на опера" тивную память.
Протокол динамического обмена данными (Dynamic Data Exchange, DDE), определяющий все основные функции для обмена данными между приложе" ниями. DDE широко использовался до тех пор, пока Microsoft не приняла ре" шение использовать OLE, которую затем переименовали в ActiveX, в качестве основной технологии взаимодействия программ.
Удаленный вызов процедур (Remote Procedure Call, RPC), строго говоря, не является механизмом IPC. Это скорее, технологическая оболочка, расширяю" щая возможности традиционных механизмов IPC. Благодаря RPC сеть стано" вится совершенно прозрачной как для сервера, так и для клиента.
ActiveX является универсальной технологией, и одним из ее применений яв" ляется обмен данными между процессами. Специально для этой цели Microsoft определила стандартный интерфейс IDataObject. А для обмена данными по сети используется Distibuted Component Object Model (DCOM), которую можно рассматривать как объединение ActiveX и RPC.
Каналы (pipes) — мощная технология обмена данными. В общем случае ка" нал можно представить в виде трубы, соединяющей два процесса, через кото"
470 |
Глава 9. Многозадачность |
|
|
рую идет непрерывный поток данных. Каналы делятся на анонимные (anonymous pipes) и именованные (named pipes). Анонимные каналы используются до" вольно редко. Именованные каналы, которые поддерживаются только в сис" темах WinNT/2000, передают произвольные данные и могут работать через сеть. В последнее время вместо именованных каналов все чаще используют сокеты.
Сокеты (sockets) — очень важная технология, так как именно она отвечает за обмен данными в сети Интернет. Сокеты также часто используются в крупных локальных сетях. Сокеты можно рассматривать как «разъемы», представляю" щие собой абстракцию конечных точек коммуникационной линии, которая со" единяет два приложения. Windows содержит достаточно мощный API для ра" боты с сокетами.
Почтовые слоты (mailslots) — это механизм однонаправленного IPC. Если при" ложению известно имя слота, то оно может помещать туда сообщения, а при" ложение, которое является владельцем этого слота, может их оттуда извлекать и обрабатывать. Основное преимущество этого способа заключается в возмож" ности передавать сообщения по локальной сети сразу нескольким компьюте" рам за одну операцию, если, конечно, несколько приемников имеют почтовые слоты с одним и тем же именем.
Microsoft Message Queue (MSMQ) — обеспечивает посылку сообщений меж" ду приложениями с помощью очереди сообщений. В отличие от других форм IPC, эта технология позволяет посылать сообщения процессу, который в дан" ное время недоступен, например, если приложение не запущено, сервер вы" шел из строя или сетевой канал связи перегружен. Механизм MSMQ ставит сообщение в очередь до тех пор, пока не появится возможность переслать его адресату.
В данной книге будут рассмотрены только два механизма обмена данными между процессами: а) обмен с помощью разделяемой памяти (или объекта ядра «проекция файла»); б) обмен при помощи сообщения WM_COPYDATA.
Но сначала — несколько слов об архитектуре памяти Win32.
Виртуальная память.
Адресное пространство процесса
Каждому процессу операционная система выделяет собственное виртуальное ад ресное пространство. В Win32 его размер составляет 4 Гбайт, что определяется разрядностью регистра команд. Соответственно, 32"битный указатель может быть любым числом в интервале от 0x00000000 до 0xFFFFFFFF. Таким образом, адре" суется 4 294 967 296 значений, что как раз и перекрывает указанный диапазон памяти.
Верхняя половина этого пространства, то есть адреса от 0x80000000 до 0xFFFFFFFF, резервируется за операционной системой, а нижняя половина почти вся доступна процессу.
Виртуальное адресное пространство процесса доступно всем потокам этого процесса. С другой стороны, потоки одного процесса не имеют доступа к адресно" му пространству других процессов.
