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

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

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

   
Объект событий (events) позволяет известить один или несколько ожидающих потоков о наступлении события. Существует два вида таких объектов.

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

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

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 может принимать одно из следующих значений:

  • EVENT_ALL_ACCESS - приложение получает полный доступ к объекту;
  • ЕVENT_MODIFY_STATE - приложение может изменять состояние объекта функциями SetEvent и ResetEvent;
  • SYNCHRONIZE (только для Windows NT) - приложение может использовать объект только в функциях ожидания.

   
После получения идентификатора можно приступать к его использованию. Для этого имеются следующие функции:

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 происходят следующие события:

  • если другие потоки не вызывали метод BeginRead или BeginWrite, то выставляется сигнал и можно обращаться к общим переменным для чтения;
  • если какие-либо из потоков выставили сигнал вызовом метода BeginRead, то данному потоку также разрешается чтение данных, при этом
    он может вытесняться другими потоками, которые осуществляют чтение данных;
  • если какой-либо из потоков выставил сигнал вызовом метода BeginWrite, то поток будет находиться в состоянии ожидания вплоть до
    вызова метода EndWrite.

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

  • если другие потоки не вызывали метод BeginRead или BeginWrite, то выставляется сигнал и можно обращаться к переменным для их модификации;
  • если другие потоки вызывали метод BeginRead или BeginWrite, то выставляется сигнал и поток будет находиться в состоянии ожидания
    до тех пор, пока все потоки, которые читают данные, не вызовут метод EndRead или пока поток, который записывает данные, не вызовет метод
    EndWrite (если во время ожидания другие потоки вызовут метод BeginRead или BeginWrite, то они тоже будут ожидать).

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

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



Вы можете оставить комментарий, или Трекбэк с вашего сайта.

Оставить комментарий