Шаг 294.
Модели потоков. Объекты синхронизации. События

    На этом шаге мы рассмотрим использование событий для синхронизации.

    Объектами синхронизации называются объекты 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 происходят следующие события:

При вызове метода BeginWrite происходят следующие события:

    Использование класса TMultiReadExclusiveWriteSynchronizer особенно эффективно на многопроцессорных компьютерах.

    На следующем шаге мы рассмотрим использование мьютексов.




Предыдущий шаг Содержание Следующий шаг