На этом шаге мы рассмотрим использование событий для синхронизации.
Объектами синхронизации называются объекты Windows, идентификаторы которых могут использоваться в функциях синхронизации. Они делятся на две группы - объекты, использующиеся только для синхронизации, и объекты, которые используются в других целях, но могут вызывать срабатывание функций ожидания. К первой группе относятся события, мьютексы и семафоры.
Объект событий (events) позволяет известить один или несколько ожидающих потоков о наступлении события. Существует два вида таких объектов.
Для создания объекта событий используется следующая функция:
function CreateEvent( lpEventAttributes: PSecurityAttributes; // Адрес структуры TSecurityAttributes bManualReset, // Указывает, будет ли объект переключаться в несигнальное состояние // вручную (True) или автоматически (False) bInitialState: BOOL: // Задает начальное состояние. Если True - // объект в сигнальном состоянии ipName: PChar // Имя или nil, если имя не требуется ): THandle; stdcall; // Возвращает идентификатор созданного объекта
Структура TSecurityAttributes описана следующим образом:
TSecurityAttributes = record nLength: DWORD; // Структура должна инициализироваться // как SizeOf(TSecurityAttributes) lpSecurityDescriptor: Pointer; // Адрес дескриптора защиты. В Windows 95 и 98 игнорируется blnhentHandle: BOOL; // Указывает, могут ли дочерние процессы наследовать объект end;
Если не требуются особые права доступа под Windows NT или наследование объекта дочерними процессами, в качестве параметра lpEventAttributes можно передавать nil. В этом случае объект не может наследоваться дочерними процессами и ему задается дескриптор защиты "по умолчанию".
Параметр lpName позволяет разделять объекты между процессами. Если lpName совпадает с именем уже существующего объекта событий, созданного текущим или любым другим процессом, функция не создает новый объект, а возвращает идентификатор уже существующего. При этом игнорируются параметры bManualReset, bInitialState и lpSecurityDescriptor. Проверить, был объект создан заново или используется уже существующий объект, можно следующим образом:
hEvent := CreateEvent (nil, TRUE, FALSE. 'EventName'); if hEvent = 0 then RaiseLastWin32Error; if GetLastError = ERROR_ALREADY_EXISTS then begin // Используем ранее созданный объект end;
Если объект используется для синхронизации внутри одного процесса, его можно объявить как глобальную переменную и создавать без имени.
Имя объекта не должно совпадать с именем любого из существующих объектов типа семафор, мьютекс, задание, таймер ожидания или файл, отображенный на память. В случае совпадения имен функция возвращает ошибку.
Если известно, что объект событий уже создан, для получения доступа к нему можно вместо CreateEvent воспользоваться функцией:
function OpenEvent( dwDesiredAccess: DWORD; // Задает права доступа к объекту bInheritHandle: BOOL; // Указывает, может ли объект наследоваться // дочерними процессами lpName: PChar // Имя объекта ): THandle; stdcall;
Функция возвращает идентификатор объекта либо 0 в случае ошибки. Параметр dwDesiredAccess может принимать одно из следующих значений:
После получения идентификатора можно приступать к его использованию. Для этого имеются следующие функции:
function SetEvent(hEvent: THandle): BOOL; stdcall; // Устанавливает объект в сигнальное состояние function ResetEvent(hEvent: THandle): BOOL; stdcall; // Устанавливает объект в несигнальное состояние function PulseEvent(hEvent: THandle): BOOL; stdcall; // Устанавливает объект в сигнальное состояние, дает // отработать всем функциям ожидания, ожидающим этот объект, // а затем снова возвращает его в несигнальное состояние
В Windows API события используются для выполнения операций асинхронного ввода-вывода. В следующем примере показано, как приложение инициирует запись одновременно в два файла, а затем ожидает завершения записи перед продолжением работы. При интенсивном вводе-выводе такой подход может обеспечить более высокую производительность, чем последовательная запись.
var Events: array[0..1] of THandle; // Массив объектов синхронизации Overlapped: array[0..1] of TOverlapped; . . . // Создаем объекты синхронизации Events[0] := CreateEvent(nil, TRUE, FALSE, nil); Events[1] := CreateEvent(nil, TRUE, FALSE, nil); // Инициализируем структуры Toverlapped FillChar(Overlapped, SizeOf(Overlapped), 0); Overlapped[0].hEvent := Events[0]; Overlapped[1].hEvent := Events[1]; // Начинаем асинхронную запись в файлы WriteFile(hFirstFile, FirstBuffer, SizeOf(FirstBuffer), FirstFileWritten, @Overlapped[0]); WriteFile(hSecondFile, SecondBuffer, SizeOf(SecondBuffer), SecondFileWritten, @Overlapped[1]); // Ожидаем завершения записи в оба файла WaitForMultipleObjects(2, @Events, True, INFINITE); // Уничтожаем объекты синхронизации CloseHandle(Events[0]); CloseHandle(Events[1]);
По завершении работы с объектом он должен быть уничтожен функцией CloseHandle.
Delphi предоставляет класс TEvent, инкапсулирующий функциональность объекта событий. Класс расположен в модуле SyncObjs.pas и объявлен следующим образом:
type TWaitResult = (wrSignaled, wrTimeout, wrAbandoned, wrError); TEvent = class(THandleObject) public constructor Create(EventAttributes: PSecurityAttributes; ManualReset.Initial State; Boolean; const Name: String); function WaitFor(Timeout: DWORD): TWaitResult; procedure SetEvent; procedure ResetEvent; end;
Назначение методов очевидно из их названий. Использование этого класса позволяет не вдаваться в тонкости реализации вызываемых функций Windows API. Для простейших случаев объявлен еще один класс с упрощенным конструктором:
type TSimpleEvent = class(TEvent) public constructor Create; end; . . . constructor TSimpleEvent.Create; begin FHandle := CreateEvent(nil, True, False, nil); end;
Прекрасный пример использования событий для синхронизации доступа можно найти в классе TMultiReadExclusiveWriteSynchronizer из модуля SysUtils.pas. Использовать этот класс желательно в тех случаях, когда несколько потоков обращаются к общим переменным, причем обращение на чтение данных осуществляется значительно чаще, чем на запись - типичная ситуация для данных, передаваемых через Интернет. Традиционные способы синхронизации доступа к данным (критические секции, мьютексы) неэффективны, поскольку, пока один из потоков не прочтет данные, все остальные находятся в состоянии ожидания, даже если им необходимы данные только для чтения. Класс TMultiReadExclusiveWriteSynchronizer решает проблему избыточной защищенности данных с помощью событий. Этот класс имеет четыре метода, которые вызываются попарно: BeginRead в паре с EndRead и BeginWrite в паре с EndWrite. Поток, который хочет прочитать данные, обязан вызвать метод BeginRead и после окончания чтения данных - метод EndRead. Соответственно, при записи данных необходимо вызвать пару методов BeginWrite и EndWrite. При вызове метода BeginRead происходят следующие события:
Использование класса TMultiReadExclusiveWriteSynchronizer особенно эффективно на многопроцессорных компьютерах.
На следующем шаге мы рассмотрим использование мьютексов.