Синхронизация процессов при работе с Windows. Waitable timer (таймер ожидания) (исходники)

Анатолий Тенцер

Таймер ожидания отсутствует в Windows 95, и для его использования необходимы Windows 98 или Windows NT 4.0 и выше.

Таймер ожидания переходит в сигнальное состояние по завершении заданного интервала времени. Для его создания используется функция CreateWaitableTimer:

function CreateWaitableTimer(
   lpTimerAttributes: PSecurityAttributes;     // Адрес структуры
                                               // TSecurityAttributes
   bManualReset: BOOL;  // Задает, будет ли таймер переходить в
                        // сигнальное состояние по завершении функции
                        // ожидания
   lpTimerName: PChar   // Имя объекта
 ): THandle; stdcall;   

Когда параметр bManualReset равен TRUE, то таймер после срабатывания функции ожидания остается в сигнальном состоянии до явного вызова SetWaitableTimer, если FALSE-таймер автоматически переходит в несигнальное состояние.

Если lpTimerName совпадает с именем уже существующего в системе таймера, то  функция возвращает его идентификатор, позволяя использовать объект для синхронизации между процессами. Имя таймера не должно совпадать с именем уже существующих объектов типов event, semaphore, mutex, job или file-mapping.

Идентификатор уже существующего таймера можно получить функцией:

function OpenWaitableTimer(
   dwDesiredAccess: DWORD;  // Задает права доступа к объекту
   bInheritHandle: BOOL;    // Задает, может ли объект наследоваться
                            // дочерними процессами
   lpTimerName: PChar       // Имя объекта
 ): THandle; stdcall;  

Параметр dwDesiredAccess может принимать следующие значения:

TIMER_ALL_ACCESS

Разрешает полный доступ к объекту

TIMER_MODIFY_STATE

Разрешает изменять состояние таймера функциями SetWaitableTimer и CancelWaitableTimer

SYNCHRONIZE

Только для Windows NT - разрешает использовать таймер в функциях ожидания

После получения идентификатора таймера поток может задать время его срабатывания функцией SetWaitableTimer:

function SetWaitableTimer(
   hTimer: THandle;                  // Идентификатор таймера
   const lpDueTime: TLargeInteger;   // Время срабатывания
   lPeriod: Longint;                 // Период повторения срабатывания
   pfnCompletionRoutine: TFNTimerAPCRoutine;  // Процедура-обработчик
   lpArgToCompletionRoutine: Pointer;// Параметр процедуры-обработчика
   fResume: BOOL                     // Задает, будет ли операционная
                                     // система «пробуждаться»
 ): BOOL; stdcall;  

Рассмотрим параметры подробнее.

lpDueTime  

Задает время срабатывания таймера. Время задается в формате TFileTime и базируется на coordinated universal time (UTC), то есть должно указываться по Гринвичу. Для преобразования системного времени в TFileTime используется функция SystemTimeToFileTime. Если время имеет положительный знак, оно трактуется как абсолютное, если отрицательный - как относительное от момента запуска таймера.

lPeriod  

Задает срок между повторными срабатываниями таймера. Если lPeriod равен 0, то  таймер сработает один раз.

pfnCompletionRoutine  

Адрес функции, объявленной как:

procedure TimerAPCProc(
   lpArgToCompletionRoutine: Pointer;  // данные
   dwTimerLowValue: DWORD;   // младшие 32 разряда значения таймера
   dwTimerHighValue: DWORD;  // старшие 32 разряда значения таймера
 ); stdcall;  

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

  • lpArgToCompletionRoutine - значение, переданное в качестве одноименного параметра в функцию SetWaitableTimer. Приложение может использовать его для передачи в процедуру обработки адреса блока данных, необходимых для ее работы
  • dwTimerLowValue и dwTimerHighValue - соответственно члены dwLowDateTime и dwHighDateTime структуры TFileTime. Они описывают время срабатывания таймера. Время задается в UTC-формате (по Гринвичу).

Если дополнительная функция обработки не нужна, в качестве этого параметра можно передать NIL.

lpArgToCompletionRoutine  

Это значение передается в функцию pfnCompletionRoutine при ее вызове.

fResume  

Определяет необходимость «пробуждения» системы, если на момент срабатывания таймера она находится в режиме экономии электроэнергии (suspended). Если операционная система не поддерживает пробуждение и fResume равно TRUE, то функция SetWaitableTimer выполнится успешно, однако последующий вызов GetLastError вернет результат ERROR_NOT_SUPPORTED.

Если необходимо перевести таймер в неактивное состояние, это можно сделать функцией:

function CancelWaitableTimer(hTimer: THandle): BOOL; stdcall;  

Эта функция не изменяет состояния таймера и не приводит к срабатыванию функций ожидания и вызову процедур-обработчиков.

По завершении работы объект должен быть уничтожен функцией CloseHandle.

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

unit WaitThread;
 
 interface
 
 uses Classes, Windows;
 
 type
   TWaitThread = class(TThread)
     WaitUntil: TDateTime;
     procedure Execute; override;
   end;
 
 implementation
 
 uses SysUtils;
 
 procedure TWaitThread.Execute;
 var
   Timer: THandle;
   SystemTime: TSystemTime;
   FileTime, LocalFileTime: TFileTime;
 begin
   Timer := CreateWaitableTimer(NIL, FALSE, NIL);
   try
     DateTimeToSystemTime(WaitUntil, SystemTime);
     SystemTimeToFileTime(SystemTime, LocalFileTime);
     LocalFileTimeToFileTime(LocalFileTime, FileTime);
     SetWaitableTimer(Timer, TLargeInteger(FileTime), 0,
       NIL, NIL, FALSE);
     WaitForSingleObject(Timer, INFINITE);
   finally
     CloseHandle(Timer);
   end;
 end;
 
 end.  

Использовать этот класс можно, например, следующим образом:

type
   TForm1 = class(TForm)
     Button1: TButton;
     procedure Button1Click(Sender: TObject);
   private
     procedure TimerFired(Sender: TObject);
   end;
 
 ...
 
 
   

implementation
 
 uses WaitThread;
 
 procedure TForm1.Button1Click(Sender: TObject);
 var
   T: TDateTime;
 begin
   with TWaitThread.Create(TRUE) do
   begin
     OnTerminate := TimerFired;
     FreeOnTerminate := TRUE;
     // Срок ожидания закончится через 5 секунд
     WaitUntil := Now + 1 / 24 / 60 / 60 * 5;
     Resume;
   end;
 end;
 
 procedure TForm1.TimerFired(Sender: TObject);
 begin
   ShowMessage('Timer fired !');
 end;  

Дополнительные объекты синхронизации

Некоторые объекты Win32 API не предназначены исключительно для целей синхронизации, однако могут использоваться с функциями синхронизации. Такими объектами являются:

Сообщение об изменении папки (change notification)

Windows позволяет организовать слежение за изменениями объектов файловой системы. Для этого служит функция FindFirstChangeNotification:

function FindFirstChangeNotification(
   lpPathName: PChar;     // Путь к папке, изменения в которой нас
                          // интересуют
   bWatchSubtree: BOOL;   // Задает необходимость слежения за
                          // изменениями во вложенных папках
   dwNotifyFilter: DWORD  // Фильтр событий
 ): THandle; stdcall;  

Параметр dwNotifyFilter - это битовая маска из одного или нескольких следующих значений:

FILE_NOTIFY_CHANGE_FILE_NAME 
 
Слежение ведется за любым изменением имени файла, в том числе за созданием и удалением файлов
FILE_NOTIFY_CHANGE_DIR_NAME 
 
Слежение ведется за любым изменением имени папки, в том числе за созданием и удалением папок
FILE_NOTIFY_CHANGE_ATTRIBUTES 
 
Слежение ведется за любым изменением атрибутов
FILE_NOTIFY_CHANGE_SIZE 
 
Слежение ведется за изменением размера файлов. Изменение размера происходит при записи в файл. Функция ожидания срабатывает только после успешного сброса дискового кэша
FILE_NOTIFY_CHANGE_LAST_WRITE 
 
Слежение ведется за изменением времени последней записи в файл, то есть фактически за любой записью в файл. Функция ожидания срабатывает только после успешного сброса дискового кэша
FILE_NOTIFY_CHANGE_SECURITY 
 
Слежение ведется за любыми изменениями дескрипторов защиты

Идентификатор, возвращенный этой функцией, может использоваться в любой функции ожидания. Он переходит в сигнальное состояние, когда в папке происходят изменения, запрошенные для слежения.  Можно продолжить слежение, используя функцию FindNextChangeNotification:

function FindNextChangeNotification(
   hChangeHandle: THandle
 ): BOOL; stdcall;  

По завершении работы идентификатор должен быть закрыт при помощи функции FindCloseChangeNotification:

function FindCloseChangeNotification(
   hChangeHandle: THandle
 ): BOOL; stdcall;  

Чтобы не блокировать исполнение основного потока программы функцией ожидания, удобно реализовать ожидание изменений в отдельном потоке. Реализуем поток на базе класса TThread. Для того чтобы можно было прервать исполнение потока методом Terminate, необходимо, чтобы функция ожидания, реализованная в методе Execute, также прерывалась при вызове Terminate. Для этого будем использовать вместо WaitForSingleObject функцию WaitForMultipleObjects и прерывать ожидание по событию (event), устанавливаемому в Terminate:

type
   TCheckFolder = class(TThread)
   private
     FOnChange: TNotifyEvent;
     Handles: array[0..1] of THandle;  // Идентификаторы объектов
                                       // синхронизации
     procedure DoOnChange;
   protected
     procedure Execute; override;
   public
     constructor Create(CreateSuspended: Boolean;
       PathToMonitor: String; WaitSubTree: Boolean;
       OnChange: TNotifyEvent; NotifyFilter: DWORD);
     destructor Destroy; override;
     procedure Terminate;
   end;
 
   

procedure TCheckFolder.DoOnChange;
 // Эта процедура вызывается в контексте главного потока приложения
 // В ней можно использовать вызовы VCL, изменять состояние формы,
 // например перечитать содержимое TListBox, отображающего файлы
 begin
   if Assigned(FOnChange) then
     FOnChange(Self);
 end;
 
   

procedure TCheckFolder.Terminate;
 begin
   inherited; // Вызываем TThread.Terminate, устанавливаем
              // Terminated = TRUE
   SetEvent(Handles[1]);  // Сигнализируем о необходимости
                          // прервать ожидание
 end;
 
   

constructor TCheckFolder.Create(CreateSuspended: Boolean;
       PathToMonitor: String; WaitSubTree: Boolean;
       OnChange: TNotifyEvent; NotifyFilter: DWORD);
 var
   BoolForWin95: Integer;
 begin
   // Создаем поток остановленным
   inherited Create(TRUE);
   // Windows 95 содержит не очень корректную реализацию функции
   // FindFirstChangeNotification. Для корректной работы необходимо,
   // чтобы:
   // - lpPathName - не содержал завершающего слэша "\" для
   //                некорневого каталога
   // - bWatchSubtree - TRUE должен передаваться как BOOL(1)
   if WaitSubTree then
     BoolForWin95 := 1
   else
     BoolForWin95 := 0;
   if (Length(PathToMonitor) > 1) and
      (PathToMonitor[Length(PathToMonitor)] = ‘\’) and
      (PathToMonitor[Length(PathToMonitor)-1] <> ‘:’) then
      Delete(PathToMonitor, Length(PathToMonitor), 1);
   Handles[0] := FindFirstChangeNotification(
     PChar(PathToMonitor), BOOL(BoolForWin95), NotifyFilter);
   Handles[1] := CreateEvent(NIL, TRUE, FALSE, NIL);
   FOnChange := OnChange;
   // И, при необходимости, запускаем
   if not CreateSuspended then
     Resume;
 end;
 
   

destructor TCheckFolder.Destroy;
 begin
   FindCloseChangeNotification(Handles[0]);
   CloseHandle(Handles[1]);
   inherited;
 end;
 
   

procedure TCheckFolder.Execute;
 var
   Reason: Integer;
   Dummy: Integer;
 begin
   repeat
     // Ожидаем изменения в папке либо сигнала о завершении
     // потока
     Reason := WaitForMultipleObjects(2, @Handles, FALSE, INFINITE);
     if Reason = WAIT_OBJECT_0 then begin
       // Изменилась папка, вызываем обработчик в контексте
       // главного потока приложения
       Synchronize(DoOnChange);
       // И продолжаем поиск
       FindNextChangeNotification(Handles[0]);
     end;
   until Terminated;
 end;  

Поскольку метод TThread.Terminate не виртуальный, этот класс нельзя использовать с переменной типа TThread, так как в этом случае будет вызываться метод Terminate класса TThread, который не может прервать ожидания, и поток будет выполняться до изменения в папке, за которой ведется слежение.

Устройство стандартного ввода с консоли (console input)

Идентификатор стандартного устройства ввода с консоли, полученный при помощи вызова функции GetStdHandle(STD_INPUT_HANDLE), можно использовать в функциях ожидания. Он находится в сигнальном состоянии, если очередь ввода консоли не пустая, и в несигнальном - если пустая. Это позволяет организовать ожидание ввода символов или при помощи функции WaitForMultipleObjects совместить его с ожиданием каких-либо других событий.

Задание (Job)

Job - это новый механизм Windows 2000, позволяющий объединить группу процессов в одно задание и манипулировать ими одновременно. Идентификатор задания находится в сигнальном состоянии, если все процессы, ассоциированные с ним, завершились по причине истечения лимита времени на выполнение задания.

Процесс (Process)

Идентификатор процесса, полученный при помощи функции CreateProcess, переходит в сигнальное состояние по завершении процесса, что позволяет организовать ожидание завершения процесса, например, при запуске из приложения внешней программы:

var
   PI: TProcessInformation;
   SI: TStartupInfo;
 
 ...
 
   FillChar(SI, SizeOf(SI), 0);
   SI.cb := SizeOf(SI);
   Win32Check(CreateProcess(NIL, 'COMMAND.COM', NIL,
     NIL, FALSE, 0, NIL, NIL, SI, PI));
   // Задерживаем исполнение программы до завершения процесса
   WaitForSingleObject(PI.hProcess, INFINITE);
   CloseHandle(PI.hProcess);
   CloseHandle(PI.hThread);  

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

Поток (thread)

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

Дополнительные механизмы синхронизации

Критические секции

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

procedure InitializeCriticalSection(
   var lpCriticalSection: TRTLCriticalSection
 ); stdcall;  

После создания объекта поток, перед доступом к защищаемому ресурсу, должен вызвать функцию:

procedure EnterCriticalSection(
   var lpCriticalSection: TRTLCriticalSection
 ); stdcall;  

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

Поток, владеющий критической секцией, может повторно вызывать функцию EnterCriticalSection без блокирования своего исполнения. По завершении работы с защищаемым ресурсом поток должен вызвать функцию LeaveCriticalSection:

procedure LeaveCriticalSection(
   var lpCriticalSection: TRTLCriticalSection
 ); stdcall;  

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

Имеется возможность попытаться захватить объект без замораживания потока. Для этого служит функция TryEnterCriticalSection:

function TryEnterCriticalSection(
   var lpCriticalSection: TRTLCriticalSection
 ): BOOL; stdcall;  

Она проверяет, захвачена ли секция в момент ее вызова. Если да - функция возвращает FALSE, в противном случае - захватывает секцию и возвращает TRUE.

По завершении работы с критической секцией она должна быть уничтожена вызовом функции DeleteCriticalSection:

procedure DeleteCriticalSection(
   var lpCriticalSection: TRTLCriticalSection
 ); stdcall;  

Рассмотрим пример приложения, осуществляющего в нескольких потоках загрузку данных по сети. Глобальные переменные BytesSummary и TimeSummary хранят общее количество загруженных байтов и время загрузки. Эти переменные каждый поток обновляет по мере считывания данных; для предотвращения конфликтов приложение должно защитить общий ресурс при помощи критической секции:

var
   // Глобальные переменные
   CriticalSection: TRTLCriticalSection;
   BytesSummary: Cardinal;
   TimeSummary: TDateTime;
   AverageSpeed: Float;
 
 ...
 
 // При инициализации приложения
 InitializeCriticalSection(CriticalSection);
 BytesSummary := 0;
 TimeSummary := 0;
 AverageSpeed := 0;
 
 
 //В методе Execute потока, загружающего данные.
 repeat
   BytesRead := ReadDataBlockFromNetwork;
   EnterCriticalSection(CriticalSection);
   try
     BytesSummary := BytesSummary + BytesRead;
     TimeSummary := TimeSummary + (Now - ThreadStartTime);
     if TimeSummary > 0 then
       AverageSpeed := BytesSummary / (TimeSummary/24/60/60);
   finally
     LeaveCriticalSection(CriticalSection)
   end;
 until LoadComplete;
 
 // При завершении приложения
 DeleteCriticalSection(CriticalSection);  

Delphi предоставляет класс, инкапсулирующий функциональность критической секции. Класс объявлен в модуле SyncObjs.pas:

type
   TCriticalSection = class(TSynchroObject)
   public
     constructor Create;
     destructor Destroy; override;
     procedure Acquire; override;
     procedure Release; override;
     procedure Enter;
     procedure Leave;
   end;  

Методы Enter и Leave являются синонимами методов Acquire и Release соответственно и добавлены для лучшей читаемости исходного кода:

procedure TCriticalSection.Enter;
 begin
   Acquire;
 end;
 
   

procedure TCriticalSection.Leave;
 begin
   Release;
 end;

Защищенный доступ к переменным (Interlocked Variable Access)

Часто возникает необходимость в совершении операций над разделяемыми между потоками 32-разрядными переменными. В целях упрощения решения этой задачи Windows API предоставляет функции для защищенного доступа к ним, не требующие использования дополнительных (и более сложных) механизмов синхронизации. Переменные, используемые в этих функциях, должны быть выровнены на границу 32-разрядного слова. Применительно к Delphi это означает, что если переменная объявлена внутри записи (record), то эта запись не должна быть упакованной (packed) и при ее объявлении должна быть активна директива компилятора {$A+}. Несоблюдение данного требования может привести к возникновению ошибок на многопроцессорных конфигурациях.

type
   TPackedRecord = packed record
     A: Byte;
     B: Integer;
   end;  
 // TPackedRecord.B нельзя использовать в функциях InterlockedXXX
 
   TNotPackedRecord = record
     A: Byte;
     B: Integer;
   end;
 
   

{$A-}
 var
   A1: TNotPackedRecord;
 // A1.B нельзя использовать в функциях InterlockedXXX
   I: Integer
 // I можно использовать в функциях InterlockedXXX, так как переменные в
 // Delphi всегда выравниваются на границу слова безотносительно
 // к состоянию директивы компилятора $A
 
   

{$A+}
 var
   A2: TNotPackedRecord;
 // A2.B можно использовать в функциях InterlockedXXX
 
   

function InterlockedIncrement(
   var Addend: Integer
 ): Integer; stdcall;  

Функция увеличивает переменную Addend на 1. Возвращаемое значение зависит от операционной системы:

Windows 98, Windows NT 4.0 и старше - возвращается новое значение переменной Addend;

Windows 95, Windows NT 3.51:

  • если после изменения Addend < 0, то возвращается отрицательное число, не обязательно равное Addend;
  • если Addend = 0, то возвращается 0;
  • если после изменения Addend > 0, то возвращается положительное число, не обязательно равное Addend.

function InterlockedDecrement(
   var Addend: Integer
 ): Integer; stdcall;  

Функция уменьшает переменную Addend на 1. Возвращаемое значение аналогично функции InterlockedIncrement.

function InterlockedExchange(
   var Target: Integer;
   Value: Integer
 ): Integer; stdcall;  

Функция записывает в переменную Target значение Value и возвращает предыдущее значение Target.

Следующие функции для выполнения требуют Windows 98 или Windows NT 4.0 и старше.

function InterlockedCompareExchange(
   var Destination: Pointer;
   Exchange: Pointer;
   Comperand: Pointer
 ): Pointer; stdcall;  

Функция сравнивает значения Destination и Comperand. Если они совпадают, значение Exchange записывается в Destination. Функция возвращает начальное значение Destination.

function InterlockedExchangeAdd(
   Addend: PLongint;
   Value: Longint
 ): Longint; stdcall;  

Функция добавляет к переменной, на которую указывает Addend, значение Value и возвращает начальное значение Addend.

Резюме

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

  1. Если приложения или потоки одного процесса изменяют общий ресурс -  защищайте доступ к нему при помощи критических секций или мьютексов.
  2. Если доступ осуществляется только на чтение - защищать ресурс не обязательно
  3. Критические секции более эффективны, но применимы только внутри одного процесса; мьютексы могут использоваться для синхронизации между процессами.
  4. Используйте семафоры для ограничения количества обращений к одному ресурсу.
  5. Используйте события (event) для информирования потока о наступлении какого-либо события.
  6. Если разделяемый ресурс - 32-битная переменная, то для синхронизации доступа к нему можно использовать функции, обеспечивающие разделяемый доступ к переменным.
  7. Многие объекты Win32 позволяют организовать эффективное слежение за своим состоянием при помощи функций ожидания. Это наиболее эффективный с точки зрения расхода системных ресурсов метод.
  8. Если ваш поток создает (даже неявно, при помощи CoInitialize или функций DDE) окна, то он должен обрабатывать сообщения. Не используйте в таком потоке функции, не позволяющие прервать ожидание по приходу сообщения с большим или неограниченным периодом ожидания. Используйте функции MsgWaitForXXX.

Страница сайта http://test.interface.ru
Оригинал находится по адресу http://test.interface.ru/home.asp?artId=4137