Синхронизация процессов при работе с 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:
|
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 предоставляет широкие возможности для написания высокоэффективных приложений. Однако написание приложений, использующих многопоточность и взаимодействующих друг с другом, при неаккуратном программировании может привести к их неверной работе, неоправданной загрузке и даже к блокировке всей операционной системы. Во избежание этого следуйте нижеприведенным рекомендациям:
- Если приложения или потоки одного процесса изменяют общий ресурс — защищайте доступ к нему при помощи критических секций или мьютексов.
- Если доступ осуществляется только на чтение — защищать ресурс не обязательно
- Критические секции более эффективны, но применимы только внутри одного процесса; мьютексы могут использоваться для синхронизации между процессами.
- Используйте семафоры для ограничения количества обращений к одному ресурсу.
- Используйте события (event) для информирования потока о наступлении какого-либо события.
- Если разделяемый ресурс — 32-битная переменная, то для синхронизации доступа к нему можно использовать функции, обеспечивающие разделяемый доступ к переменным.
- Многие объекты Win32 позволяют организовать эффективное слежение за своим состоянием при помощи функций ожидания. Это наиболее эффективный с точки зрения расхода системных ресурсов метод.
- Если ваш поток создает (даже неявно, при помощи CoInitialize или функций DDE) окна, то он должен обрабатывать сообщения. Не используйте в таком потоке функции, не позволяющие прервать ожидание по приходу сообщения с большим или неограниченным периодом ожидания. Используйте функции MsgWaitForXXX.
КомпьютерПресс 9'2001