Синхронизация процессов при работе с Windows

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

Функции синхронизации

   Функции, ожидающие единственный объект

   Функции, ожидающие несколько объектов

   Прерывание ожидания по запросу на завершение операции ввода-вывода или APC

Объекты синхронизации

   Event (событие)

   Mutex (Mutually Exclusive)

   Semaphore (семафор)

   Waitable timer (таймер ожидания)

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

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

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

      Задание (Job)

      Процесс (Process)

      Поток (thread)

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

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

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

Резюме

 

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

Главной идеей, заложенной в основе синхронизации потоков в Win32, является использование объектов синхронизации и функций ожидания. Объекты могут находиться в одном из двух состояний — Signaled или Not Signaled. Функции ожидания блокируют выполнение потока до тех пор, пока заданный объект находится в состоянии Not Signaled. Таким образом, поток, которому необходим эксклюзивный доступ к ресурсу, должен выставить какой-либо объект синхронизации в несигнальное состояние, а по окончании — сбросить его в сигнальное. Остальные потоки должны перед доступом к этому ресурсу вызвать функцию ожидания, которая позволит им дождаться освобождения ресурса.

Рассмотрим, какие объекты и функции синхронизации предоставляет нам Win32 API.

Функции синхронизации

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

В начало В начало

Функции, ожидающие единственный объект

Простейшей функцией ожидания является функция WaitForSingleObject:

function WaitForSingleObject(
     hHandle: THandle;       // идентификатор    объекта
     dwMilliseconds: DWORD   // период ожидания
   ): DWORD; stdcall; 

Функция ожидает перехода объекта hHandle в сигнальное состояние в течение dwMilliseconds миллисекунд. Если в качестве параметра dwMilliseconds передать значение INFINITE, функция будет ждать в течение неограниченного времени. Если dwMilliseconds равен 0, то функция проверяет состояние объекта и немедленно возвращает управление.

Функция возвращает одно из следующих значений:

WAIT_ABANDONED 
Поток, владевший объектом, завершился, не переведя объект в сигнальное состояние
WAIT_OBJECT_0 
Объект перешел в сигнальное состояние
WAIT_TIMEOUT 
Истек срок ожидания. Обычно в этом случае генерируется ошибка либо функция вызывается в цикле до получения другого результата
WAIT_FAILED 
Произошла ошибка (например, получено неверное значение hHandle). Более подробную информацию можно получить, вызвав GetLastError

Следующий фрагмент кода запрещает доступ к Action1 до перехода объекта ObjectHandle в сигнальное состояние (например, таким образом можно дожидаться завершения процесса, передав в качестве ObjectHandle его идентификатор, полученный функцией CreateProcess):

var
     Reason: DWORD;
     ErrorCode: DWORD;
   
   Action1.Enabled := FALSE;
   try
     repeat
       Application.ProcessMessages;
       Reason := WailForSingleObject(ObjectHandle, 10);
       if Reason = WAIT_FAILED then begin
         ErrorCode := GetLastError;
         raise Exception.CreateFmt(
           ‘Wait for object failed with error:    %d’, [ErrorCode]);
       end;
     until Reason <> WAIT_TIMEOUT;
   finally
     Actionl.Enabled := TRUE;
   end; 

В случае когда одновременно с ожиданием объекта требуется перевести в сигнальное состояние другой объект, может использоваться функция SignalObjectAndWait:

function SignalObjectAndWait(
     hObjectToSignal: THandle;  // объект, который будет переведен в
                                   // сигнальное состояние
     hObjectToWaitOn: THandle;  // объект, который     ожидает    функция
     dwMilliseconds: DWORD;     // период ожидания
     bAlertable: BOOL              // задает, должна ли функция возвращать
                                //    управление в случае запроса на
                                   // завершение операции ввода-вывода
   ): DWORD; stdcall; 

Возвращаемые значения аналогичны функции WaitForSingleObject.

! В модуле Windows.pas эта функция ошибочно объявлена как возвращающая значение BOOL. Если вы намерены ее использовать – объявите ее корректно или используйте приведение типа возвращаемого значения к DWORD.

Объект hObjectToSignal может быть семафором, событием (event) либо мьютексом. Параметр bAlertable определяет, будет ли прерываться ожидание объекта в случае, если операционная система запросит у потока окончание операции асинхронного ввода-вывода либо асинхронный вызов процедуры. Более подробно это будет рассматриваться ниже.

В начало В начало

Функции, ожидающие несколько объектов

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

type
     TWOHandleArray = array[0..MAXIMUM_WAIT_OBJECTS - 1] of THandle;
     PWOHandleArray = ^TWOHandleArray;
   
   function WaitForMultipleObjects(
     nCount: DWORD;                 // Задает количество объектов
     lpHandles: PWOHandleArray;  // Адрес массива объектов
     bWaitAll: BOOL;                // Задает, требуется ли ожидание всех
                                    // объектов или любого
     dwMilliseconds: DWORD       // Период ожидания
   ): DWORD; stdcall; 

Функция возвращает одно из следующих значений:

Число в диапазоне от

       WAIT_OBJECT_0 до
       WAIT_OBJECT_0 + nCount – 1              
Если bWaitAll равно TRUE, то это число означает, что все объекты перешли в сигнальное состояние. Если FALSE — то, вычтя из возвращенного значения WAIT_OBJECT_0, мы получим индекс объекта в массиве lpHandles

Число в диапазоне от

       WAIT_ABANDONED_0 до
       WAIT_ABANDONED_0 + nCount – 1              
Если bWaitAll равно TRUE,  это означает, что все объекты перешли в сигнальное состояние, но хотя бы один из владевших ими потоков завершился, не сделав объект сигнальным Если FALSE — то, вычтя из возвращенного значения WAIT_ABANDONED_0,  мы получим в массиве lpHandles индекс объекта, при этом поток, владевший этим объектом, завершился,  не сделав его сигнальным
WAIT_TIMEOUT 
Истек период ожидания
WAIT_FAILED 
Произошла ошибка

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

var
     Handles: array[0..1] of THandle;
     Reason: DWORD;
     RestIndex: Integer;
   
   ...
   
   Handles[0] := OpenMutex(SYNCHRONIZE, FALSE, ‘FirstResource’);
   Handles[1] := OpenMutex(SYNCHRONIZE, FALSE, ‘SecondResource’);
   // Ждем первый из объектов
   Reason := WaitForMultipleObjects(2, @Handles, FALSE, INFINITE);
   case Reason of
     WAIT_FAILED: RaiseLastWin32Error;
     WAIT_OBJECT_0, WAIT_ABANDONED_0:
       begin
         ModifyFirstResource;
         RestIndex := 1;
       end;
     WAIT_OBJECT_0 + 1, WAIT_ABANDONED_0 + 1:
       begin
         ModifySecondResource;
         RestIndex := 0;
       end;
     // WAIT_TIMEOUT возникнуть не может
   end;
   // Теперь ожидаем освобождения следующего объекта
   if WailForSingleObject(Handles[RestIndex],
        INFINITE) = WAIT_FAILED then
          RaiseLastWin32Error;
   // Дождались, модифицируем оставшийся ресурс
   if RestIndex = 0 then
     ModifyFirstResource
   else
    ModifySecondResource; 

Описанную выше технику можно применять, если вы точно знаете, что задержка ожидания объекта будет незначительной. В противном случае ваша программа окажется «замороженной» и не сможет даже перерисовать свое окно. Если период задержки может оказаться значительным, то необходимо дать программе возможность реагировать на сообщения Windows. Выходом может стать использование функций с ограниченным периодом ожидания (и повторный вызов — в случае возврата WAIT_TIMEOUT) либо функции MsgWaitForMultipleObjects:

function MsgWaitForMultipleObjects(
   nCount: DWORD;     // количество объектов синхронизации
   var pHandles;      // адрес массива объектов
   fWaitAll: BOOL;    // Задает, требуется ли ожидание всех
                      // объектов или любого
   dwMilliseconds,    // Период ожидания
   dwWakeMask: DWORD  // Тип события, прерывающего ожидание
 ): DWORD; stdcall;  

Главное отличие этой функции от предыдущей — параметр dwWakeMask, который является комбинацией битовых флагов QS_XXX и задает типы сообщений, прерывающих ожидание функции независимо от состояния ожидаемых объектов. Например, маска QS_KEY позволяет прервать ожидание при появлении в очереди сообщений WM_KEYUP, WM_KEYDOWN, WM_SYSKEYUP или WM_SYSKEYDOWN, а маска QS_PAINT — сообщения WM_PAINT. Полный список значений, допустимых для dwWakeMask, имеется в документации по Windows SDK. При появлении в очереди потока, вызвавшего функцию, сообщений, соответствующих заданной маске, функция возвращает значение WAIT_OBJECT_0 + nCount. Получив это значение, ваша программа может обработать его и снова вызвать функцию ожидания. Рассмотрим пример с запуском внешнего приложения (необходимо, чтобы на время его работы вызывающая программа не реагировала на ввод пользователя, однако ее окно должно продолжать перерисовываться):

procedure TForm1.Button1Click(Sender: TObject);
 var
   PI: TProcessInformation;
   SI: TStartupInfo;
   Reason: DWORD;
   Msg: TMsg;
 begin
   // Инициализируем структуру 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);
 // и посмотреть, как будет реагировать программа на
 // перемещение других окон над ее окном
 //**************************************************
   repeat
     // Ожидаем завершения дочернего процесса или сообщения
     // перерисовки WM_PAINT
     Reason := MsgWaitForMultipleObjects(1, PI.hProcess, FALSE,
       INFINITE, QS_PAINT);
     if Reason = WAIT_OBJECT_0 + 1 then begin
       // В очереди сообщений появился WM_PAINT – Windows
       // требует обновить окно программы.
       // Удаляем сообщение из очереди
       PeekMessage(Msg, 0, WM_PAINT, WM_PAINT, PM_REMOVE);
       // И перерисовываем наше окно
       Update;
     end;
     // Повторяем цикл, пока не завершится дочерний процесс
   until Reason = WAIT_OBJECT_0;
   // Удаляем из очереди накопившиеся там сообщения
   while PeekMessage(Msg, 0, 0, 0, PM_REMOVE) do;
   CloseHandle(PI.hProcess);
   CloseHandle(PI.hThread)
 end;  

Если в потоке, вызывающем функции ожидания, явно (функцией CreateWindow) или неявно (используя TForm, DDE, COM) создаются окна Windows — поток должен обрабатывать сообщения. Поскольку широковещательные сообщения посылаются всем окнам в системе, то поток, не обрабатывающий сообщения, может вызвать взаимоблокировку (система ждет, когда поток обработает сообщение, поток — когда система или другие потоки освободят объект) и привести к зависанию Windows. Если в вашей программе имеются подобные фрагменты, необходимо использовать MsgWaitForMultipleObjects или MsgWaitForMultipleObjectsEx и позволять прервать ожидание для обработки сообщений. Алгоритм аналогичен вышеприведенному примеру.

В начало В начало

Прерывание ожидания по запросу на завершение операции ввода-вывода или APC

Windows поддерживает асинхронные вызовы процедур. При создании каждого потока (thread) с ним ассоциируется очередь асинхронных вызовов процедур (APC queue). Операционная система (или приложение пользователя — при помощи функции QueueUserAPC) может помещать в нее запросы на выполнение функций в контексте данного потока. Эти функции не могут быть выполнены немедленно, поскольку поток может быть занят. Поэтому операционная система вызывает их, когда поток вызывает одну из следующих функций ожидания:

function SleepEx(
   dwMilliseconds: DWORD;   // Период ожидания
   bAlertable: BOOL         // задает, должна ли функция возвращать
                            // управление в случае запроса на
                            // асинхронный вызов процедуры
 ): DWORD; stdcall;
 
   
function WaitForSingleObjectEx(
   hHandle: THandle;      // Идентификатор объекта
   dwMilliseconds: DWORD; // Период ожидания
   bAlertable: BOOL       // задает, должна ли функция возвращать
                          // управление в случае запроса на
                          // асинхронный вызов процедуры
 ): DWORD; stdcall;
 
   
function WaitForMultipleObjectsEx(
   nCount: DWORD;            // количество объектов
   lpHandles: PWOHandleArray;// адрес массива идентификаторов объектов
   bWaitAll: BOOL;           // Задает, требуется ли ожидание всех
                             // объектов или любого
   dwMilliseconds: DWORD;    // Период ожидания
   bAlertable: BOOL          // задает, должна ли функция возвращать
                             // управление в случае запроса на
                             // асинхронный вызов процедуры
 ): DWORD; stdcall;
 
   
function SignalObjectAndWait(
   hObjectToSignal: THandle;  // объект, который будет переведен в
                              // сигнальное состояние
   hObjectToWaitOn: THandle;  // объект, которого ожидает функция
   dwMilliseconds: DWORD;     // период ожидания
   bAlertable: BOOL           // задает, должна ли функция возвращать
                              // управление в случае запроса на
                              // асинхронный вызов процедуры
 ): DWORD; stdcall;  
   
function MsgWaitForMultipleObjectsEx(
 nCount: DWORD;     // количество объектов синхронизации
   var pHandles;      // адрес массива объектов
   fWaitAll: BOOL;    // Задает, требуется ли ожидание всех
                      // объектов или любого
   dwMilliseconds,    // Период ожидания
   dwWakeMask: DWORD  // Тип события, прерывающего ожидание
   dwFlags: DWORD     // Дополнительные флаги
 ): DWORD; stdcall;  

Если параметр bAlertable равен TRUE (либо если dwFlags в функции MsgWaitForMultipleObjectsEx содержит MWMO_ALERTABLE), то при появлении в очереди APC запроса на асинхронный вызов процедуры операционная система выполняет вызовы всех имеющихся в очереди процедур, после чего функция возвращает значение WAIT_IO_COMPLETION.

Такой механизм позволяет реализовать, например, асинхронный ввод-вывод. Поток может инициировать фоновое выполнение одной или нескольких операций ввода-вывода функциями ReadFileEx или WriteFileEx, передав им адреса функций-обработчиков завершения операции. По завершении вызовы этих функций будут поставлены в очередь асинхронного вызова процедур. В свою очередь, инициировавший операции поток, когда он будет готов обработать результаты, может, используя одну из вышеприведенных функций ожидания, позволить операционной системе вызвать функции-обработчики. Поскольку очередь APC реализована на уровне ядра ОС, она более эффективна, чем очередь сообщений, и позволяет реализовать гораздо более эффективный ввод-вывод.

В начало В начало

Объекты синхронизации

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

В начало В начало

Event (событие)

Event позволяет известить один или несколько ожидающих потоков о наступлении события. Event бывает:

Отключаемый вручную

Будучи установленным в сигнальное состояние, остается в нем до тех пор, пока не будет переключен явным вызовом функции ResetEvent

Автоматически отключаемый

Автоматически переключается в несигнальное состояние операционной системой, когда один из ожидающих его потоков завершается

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

function CreateEvent(
   lpEventAttributes: PSecurityAttributes;  // Адрес структуры
                                            // TSecurityAttributes
    bManualReset,         // Задает, будет Event переключаемым
                          // вручную (TRUE) или автоматически (FALSE)
    bInitialState: BOOL;  // Задает начальное состояние. Если TRUE -
                          // объект в сигнальном состоянии
    lpName: PChar         // Имя или NIL, если имя не требуется
 ): THandle; stdcall;     // Возвращает идентификатор созданного
                          // объекта  
Структура TSecurityAttributes описана, как:  
TSecurityAttributes = record
   nLength: DWORD;                // Размер структуры, должен
                                  // инициализироваться как
                                  // SizeOf(TSecurityAttributes)
   lpSecurityDescriptor: Pointer; // Адрес дескриптора защиты. В
                                  // Windows 95 и 98 игнорируется
                                  // Обычно можно указывать NIL
   bInheritHandle: BOOL;          // Задает, могут ли дочерние
                                  // процессы наследовать объект
 end;  

Если не требуется задание особых прав доступа под Windows NT или возможности наследования объекта дочерними процессами, в качестве параметра lpEventAttributes можно передавать NIL. В этом случае объект не может наследоваться дочерними процессами и ему задается дескриптор защиты «по умолчанию».

Параметр lpName позволяет разделять объекты между процессами. Если lpName совпадает с именем уже существующего объекта типа Event, созданного текущим или любым другим процессом, то функция не создает нового объекта, а возвращает идентификатор уже существующего. При этом игнорируются параметры bManualReset, bInitialState и lpSecurityDescriptor. Проверить, был ли объект создан или используется уже существующий, можно следующим образом:

hEvent := CreateEvent(NIL, TRUE, FALSE, ‘EventName’);
 if hEvent = 0 then
   RaiseLastWin32Error;
 if GetLastError = ERROR_ALREADY_EXISTS then begin
   // Используем ранее созданный объект
 end;  

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

Имя объекта не должно совпадать с именем любого из существующих объектов типов Semaphore, Mutex, Job, Waitable Timer или FileMapping. В случае совпадения имен функция возвращает ошибку.

Если известно, что Event уже создан, для получения доступа к нему можно вместо CreateEvent воспользоваться функцией OpenEvent:

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

Функция возвращает идентификатор объекта либо 0 — в случае ошибки. Параметр dwDesiredAccess может принимать одно из следующих значений:

EVENT_ALL_ACCESS              

Приложение получает полный доступ к объекту

EVENT_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, инкапсулирующий функциональность объекта Event. Класс расположен в модуле SyncObjs.pas и объявлен следующим образом:

type
   TWaitResult = (wrSignaled, wrTimeout, wrAbandoned, wrError);
 
   TEvent = class(THandleObject)
   public
     constructor Create(EventAttributes: PSecurityAttributes;
       ManualReset, InitialState: 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;  
В начало В начало

Mutex (Mutually Exclusive)

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

function CreateMutex(
   lpMutexAttributes: PSecurityAttributes;  // Адрес структуры
                                            // TSecurityAttributes
   bInitialOwner: BOOL;  // Задает, будет ли процесс владеть
                         // мьютексом сразу после создания
   lpName: PChar         // Имя мьютекса
 ): THandle; stdcall;  

Функция возвращает идентификатор созданного объекта либо 0. Если мьютекс с заданным именем уже был создан, возвращается его идентификатор. В этом случае функция GetLastError вернет код ошибки ERROR_ALREDY_EXISTS. Имя не должно совпадать с именем уже существующего объекта типов Semaphore, Event, Job, Waitable Timer или FileMapping.

Если неизвестно, существует ли уже мьютекс с таким именем, программа не должна запрашивать владение объектом при создании (то есть должна передать в качестве bInitialOwner значение FALSE).

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

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

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

MUTEX_ALL_ACCESS 
Приложение получает полный доступ к объекту
SYNCHRONIZE
Только для Windows NT — приложение может использовать объект только в функциях ожидания и функции ReleaseMutex

Функция возвращает идентификатор открытого мьютекса либо 0 — в случае ошибки. Мьютекс переходит в сигнальное состояние после срабатывания функции ожидания, в которую был передан его идентификатор. Для возврата в несигнальное состояние служит функция ReleaseMutex:

function ReleaseMutex(hMutex: THandle): BOOL; stdcall;  

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

var
   Mutex: THandle;
 
 // При инициализации программы
 Mutex := CreateMutex(NIL, FALSE, ‘UniqueMutexName’);
 if Mutex = 0 then
   RaiseLastWin32Error;
 
 ...
 // Доступ к ресурсу
 WaitForSingleObject(Mutex, INFINITE);
 try
   // Доступ к ресурсу, захват мьютекса гарантирует,
   // что остальные процессы, пытающиеся получить доступ,
   // будут остановлены на функции WaitForSingleObject
   ...
 finally
   // Работа с ресурсом окончена, освобождаем его
   // для остальных процессов
   ReleaseMutex(Mutex);
 end;
 
 ...
 // При завершении программы
 CloseHandle(Mutex);  

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

Разумеется, если работа с ресурсом может потребовать значительного времени, то необходимо либо использовать функцию MsgWaitForSingleObject, либо вызывать WaitForSingleObject в цикле с нулевым периодом ожидания, проверяя код возврата. В противном случае ваше приложение окажется замороженным. Всегда защищайте захват-освобождение объекта синхронизации при помощи блока try ... finally, иначе ошибка во время работы с ресурсом приведет к блокированию работы всех процессов, ожидающих его освобождения.

В начало В начало

Semaphore (семафор)

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

Для создания семафора служит функция CreateSemaphore:

function CreateSemaphore(
   lpSemaphoreAttributes: PSecurityAttributes; // Адрес структуры
                                               // TSecurityAttributes
   lInitialCount,           // Начальное значение счетчика
   lMaximumCount: Longint;  // Максимальное значение счетчика
   lpName: PChar            // Имя объекта
 ): THandle; stdcall;  

Функция возвращает идентификатор созданного семафора либо 0, если создать объект не удалось.

Параметр lMaximumCount задает максимальное значение счетчика семафора, lInitialCount задает начальное значение счетчика и должен быть в диапазоне от 0 до lMaximumCount. lpName задает имя семафора. Если в системе уже есть семафор с таким именем, то новый не создается, а возвращается идентификатор существующего семафора. В случае если семафор используется внутри одного процесса, можно создать его без имени, передав в качестве lpName значение NIL. Имя семафора не должно совпадать с именем уже существующего объекта типов event, mutex, waitable timer, job или file-mapping.

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

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

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

SEMAPHORE_ALL_ACCESS              

Поток получает все права на семафор

SEMAPHORE_MODIFY_STATE              

Поток может увеличивать счетчик семафора функцией ReleaseSemaphore

SYNCHRONIZE              

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

Для увеличения счетчика семафора используется функция ReleaseSemaphore:

function ReleaseSemaphore(
   hSemaphore: THandle;      // Идентификатор семафора
   lReleaseCount: Longint;   // Счетчик будет увеличен на эту величину
   lpPreviousCount: Pointer  // Адрес 32-битной переменной,
                             // принимающей предыдущее значение
                             // счетчика
 ): BOOL; stdcall;  

Если значение счетчика после выполнения функции превысит заданный для него функцией CreateSemaphore максимум, то ReleaseSemaphore возвращает FALSE и значение семафора не изменяется. В качестве параметра lpPreviousCount можно передать NIL, если это значение нам не нужно.

Рассмотрим пример приложения, запускающего на выполнение несколько заданий в отдельных потоках (например, программа для фоновой загрузки файлов из Internet). Если количество одновременно выполняющихся заданий будет слишком велико, то это приведет к неоправданной загрузке канала. Поэтому реализуем потоки, в которых будет выполняться задание, таким образом, чтобы когда их количество превышает заранее заданную величину, то поток бы останавливался и ожидал завершения работы ранее запущенных заданий:

unit LimitedThread;
 
 interface
 
 uses Classes;
 
 type
   TLimitedThread = class(TThread)
     procedure Execute; override;
   end;
  
 implementation
 
 uses Windows;
 
 const
   MAX_THREAD_COUNT = 10;
 
 var
   Semaphore: THandle;
 
 procedure TLimitedThread.Execute;
 begin
   // Уменьшаем счетчик семафора. Если к этому моменту уже запущено
   // MAX_THREAD_COUNT потоков — счетчик равен 0 и семафор в
   // несигнальном состоянии. Поток будет заморожен до завершения
   // одного из запущенных ранее.
   WaitForSingleObject(Semaphore, INFINITE);
  
   // Здесь располагается код, отвечающий за функциональность потока,
   // например загрузка файла
   ...
 
   // Поток завершил работу, увеличиваем счетчик семафора и позволяем
   // начать обработку другим потокам.
   ReleaseSemaphore(Semaphore, 1, NIL);
 end;
 
 initialization
   // Создаем семафор при старте программы
   Semaphore := CreateSemaphore(NIL, MAX_THREAD_COUNT,
     MAX_THREAD_COUNT, NIL);
 
 finalization
   // Уничтожаем семафор по завершении программы
   CloseHandle(Semaphore);
 end;  

1999 1 2 3 4 5 6 7 8 9 10 11 12
2000 1 2 3 4 5 6 7 8 9 10 11 12
2001 1 2 3 4 5 6 7 8 9 10 11 12
2002 1 2 3 4 5 6 7 8 9 10 11 12
2003 1 2 3 4 5 6 7 8 9 10 11 12
2004 1 2 3 4 5 6 7 8 9 10 11 12
2005 1 2 3 4 5 6 7 8 9 10 11 12
2006 1 2 3 4 5 6 7 8 9 10 11 12
2007 1 2 3 4 5 6 7 8 9 10 11 12
2008 1 2 3 4 5 6 7 8 9 10 11 12
2009 1 2 3 4 5 6 7 8 9 10 11 12
2010 1 2 3 4 5 6 7 8 9 10 11 12
2011 1 2 3 4 5 6 7 8 9 10 11 12
2012 1 2 3 4 5 6 7 8 9 10 11 12
2013 1 2 3 4 5 6 7 8 9 10 11 12
Популярные статьи
КомпьютерПресс использует