Функции синхронизации
Функции, ожидающие единственный объект
Функции, ожидающие несколько объектов
Прерывание ожидания по запросу на завершение операции ввода-вывода
или 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
бывает:
Отключаемый |
Будучи |
Автоматически |
Автоматически |
Для создания объекта используется функция 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 |
Приложение |
SYNCHRONIZE |
Только для Windows |
После получения идентификатора можно
приступать к его использованию. Для
этого имеются следующие функции:
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 |
Поток |
SYNCHRONIZE |
Только для Windows |
Для увеличения счетчика семафора
используется функция 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;
Provide feedback
Saved searches
Use saved searches to filter your results more quickly
Sign up
Appearance settings
Для программ, использующих несколько потоков или процессов, необходимо, чтобы все они выполняли возложенные на них функции в нужной последовательности. В среде Windows 9x для этой цели предлагается использовать несколько механизмов, обеспечивающих слаженную работу потоков. Эти механизмы называют механизмами синхронизации.
Предположим, разрабатывается программа, в которой параллельно работают два потока. Каждый поток обращается к одной разделяемой глобальной переменной. Один поток при каждом обращении к этой переменной выполняет её инкремент, а второй – декремент. При одновременной асинхронной работе потоков неизбежно возникает такая ситуация:
— первый поток прочитал значение глобальной переменной в локальную;
— ОС прерывает его, так как закончился выделенный ему квант времени процессора, и передаёт управление второму потоку;
— второй поток также считал значение глобальной переменной в локальную, декрементировал её и записал новое значение обратно;
— ОС вновь передаёт управление первому потоку, тот, ничего не зная о действиях второго потока, инкрементирует свою локальную переменную и записывает её значение в глобальную.
Очевидно, что изменения, внесённые вторым потоком, будут утеряны.
Для исключения подобных ситуаций необходимо разделить во времени использование совместных данных. В таких случаях используются механизмы синхронизации, которые обеспечивают корректную работу нескольких потоков.
Средства синхронизации в ОС Windows:
1) критическая секция (Critical Section) – это объект, который принадлежи процессу, а не ядру. А значит, не может синхронизировать потоки из разных процессов.
Существует так же функции инициализации (создания) и удаления, вхождения и выхода из критической секции:
создание – InitializeCriticalSection(…), удаление – DeleteCriticalSection(…),
вход – EnterCriticalSection(…), выход – LeaveCriticalSection(…).
Ограничения: поскольку это не объект ядра, то он не виден другим процессам, то есть можно защищать только потоки своего процесса.
Критический раздел анализирует значение специальной переменной процесса, которая используется как флаг, предотвращающий исполнение некоторого участка кода несколькими потоками одновременно.
Среди синхронизирующих объектов критические разделы наиболее просты.
2) mutex – mutable exclude. Это объект ядра, у него есть имя, а значит с их помощью можно синхронизировать доступ к общим данным со стороны нескольких процессов, точнее, со стороны потоков разных процессов. Ни один другой поток не может завладеть мьютексом, который уже принадлежит одному из потоков. Если мьютекс защищает какие-то совместно используемые данные, он сможет выполнить свою функцию только в случае, если перед обращением к этим данным каждый из потоков будет проверять состояние этого мьютекса. Windows расценивает мьютекс как объект общего доступа, который можно перевести в сигнальное состояние или сбросить. Сигнальное состояние мьютекса говорит о том, что он занят. Потоки должны самостоятельно анализировать текущее состояние мьютексов. Если требуется, чтобы к мьютексу могли обратиться потоки других процессов, ему надо присвоить имя.
Функции:
CreateMutex(имя) – создание, hnd=OpenMutex(имя) – открытие,
WaitForSingleObject(hnd) – ожидание и занятие,
ReleaseMutex(hnd) – освобождение, CloseHandle(hnd) – закрытие.
Его можно использовать в защите от повторного запуска программ.
3) семафор – semaphore. Объект ядра “семафор” используются для учёта ресурсов и служат для ограничения одновременного доступа к ресурсу нескольких потоков. Используя семафор, можно организовать работу программы таким образом, что к ресурсу одновременно смогут получить доступ несколько потоков, однако количество этих потоков будет ограничено. Создавая семафор, указывается максимальное количество потоков, которые одновременно смогут работать с ресурсом. Каждый раз, когда программа обращается к семафору, значение счетчика ресурсов семафора уменьшается на единицу. Когда значение счетчика ресурсов становится равным нулю, семафор недоступен.
создание CreateSemaphore, открытие OpenSemaphore,
занять WaitForSingleObject, освобождение ReleaseSemaphore
4) событие – event. События обычно просто оповещают об окончании какой-либо операции, они также являются объектами ядра. Можно не просто явным образом освободить, но так же есть операция установки события. События могут быть мануальными (manual) и единичными (single).
Единичное событие (single event) – это скорее общий флаг. Событие находится в сигнальном состоянии, если его установил какой-нибудь поток. Если для работы программы требуется, чтобы в случае возникновения события на него реагировал только один из потоков, в то время как все остальные потоки продолжали ждать, то используют единичное событие.
Мануальное событие (manual event) — это не просто общий флаг для нескольких потоков. Оно выполняет несколько более сложные функции. Любой поток может установить это событие или сбросить (очистить) его. Если событие установлено, оно останется в этом состоянии сколь угодно долгое время, вне зависимости от того, сколько потоков ожидают установки этого события. Когда все потоки, ожидающие этого события, получат сообщение о том, что событие произошло, оно автоматически сбросится.
Функции: SetEvent, ClearEvent, WaitForEvent.
Типы событий:
1) событие с автоматическим сбросом: WaitForSingleEvent.
2) событие с ручным сбросом (manual), тогда событие необходимо сбрасывать: ReleaseEvent.
Некоторые теоретики выделяют ещё один объект синхронизации:
WaitAbleTimer – объект ядра ОС, который самостоятельно переходит в свободное состояние через заданный интервал времени (будильник).
2024-12-03
Введение
Управление процессами в Windows API (WinAPI) включает в себя создание, управление и взаимодействие с процессами и потоками в операционной системе Windows. Ниже приведены основные функции и концепции, используемые для работы с процессами в WinAPI:
- Создание процессов:
CreateProcess
: Функция, которая создает новый процесс. Вы можете указать исполняемый файл, командную строку и другие параметры при создании процесса.
- Информация о процессе:
GetCurrentProcess
: Функция, которая возвращает дескриптор текущего процесса.OpenProcess
: Функция, которая открывает существующий процесс по его идентификатору (PID).GetProcessId
: Функция, которая возвращает идентификатор процесса для данного дескриптора процесса.
- Управление приоритетом процессов:
SetPriorityClass
: Функция, которая устанавливает приоритет выполнения процесса.GetPriorityClass
: Функция, которая возвращает текущий приоритет процесса.
- Управление потоками внутри процесса:
CreateThread
: Функция, которая создает новый поток внутри процесса.TerminateThread
: Функция, которая завершает выполнение потока.
- Синхронизация между потоками и процессами:
Mutex
,Semaphore
,Event
: Различные объекты синхронизации, которые можно использовать для синхронизации работы процессов и потоков.
- Завершение процессов:
ExitProcess
: Функция, которая завершает текущий процесс.TerminateProcess
: Функция, которая принудительно завершает другой процесс.
- Получение информации о процессах:
EnumProcesses
: Функция, которая позволяет перечислить все запущенные процессы и получить их идентификаторы.QueryFullProcessImageName
: Функция, которая возвращает путь к исполняемому файлу процесса по его идентификатору.
- Модули процесса:
EnumProcessModules
: Функция, которая позволяет перечислить все модули (библиотеки DLL) внутри процесса.
- Управление разрешениями и защитой процессов:
OpenProcessToken
,GetTokenInformation
: Функции для работы с маркерами доступа процессов и получения информации о правах доступа.
Создание процессов
Создание процессов в операционной системе Windows с использованием Windows API (WinAPI) осуществляется с помощью функции CreateProcess
. Эта функция позволяет запустить новый процесс и настроить различные атрибуты его выполнения. Вот пример использования функции CreateProcess
на языке C++:
#include <windows.h>
#include <tchar.h>
int main() {
SetConsoleOutputCP(1251);
// Имя исполняемого файла, который нужно запустить
LPCTSTR applicationName = _T("C:\\Path\\To\\Your\\Program.exe");
// Командная строка для передачи процессу
LPTSTR commandLine = NULL;
// Защитные атрибуты процесса и потока (обычно NULL)
LPSECURITY_ATTRIBUTES processAttributes = NULL;
LPSECURITY_ATTRIBUTES threadAttributes = NULL;
// Флаги создания процесса
BOOL inheritHandles = FALSE;
DWORD creationFlags = 0;
LPVOID environment = NULL;
LPCTSTR currentDirectory = NULL;
STARTUPINFO startupInfo;
PROCESS_INFORMATION processInfo;
// Заполнение структуры STARTUPINFO
ZeroMemory(&startupInfo, sizeof(STARTUPINFO));
startupInfo.cb = sizeof(STARTUPINFO);
// Создание нового процесса
BOOL result = CreateProcess(
applicationName, // Имя исполняемого файла
commandLine, // Командная строка
processAttributes, // Атрибуты процесса
threadAttributes, // Атрибуты потока
inheritHandles, // Флаг наследования дескрипторов
creationFlags, // Флаги создания процесса
environment, // Переменные окружения (обычно NULL)
currentDirectory, // Текущий рабочий каталог (обычно NULL)
&startupInfo, // Структура STARTUPINFO
&processInfo // Структура PROCESS_INFORMATION
);
if (result) {
// Процесс успешно создан
// Вы можете получить информацию о процессе, используя processInfo
// Закрыть дескрипторы, чтобы избежать утечек ресурсов
CloseHandle(processInfo.hProcess);
CloseHandle(processInfo.hThread);
} else {
// Произошла ошибка при создании процесса
DWORD error = GetLastError();
// Обработка ошибки
}
return 0;
}
Обратите внимание, что вы должны заменить C:\\Path\\To\\Your\\Program.exe
на путь к исполняемому файлу, который вы хотите запустить. Функция CreateProcess
возвращает TRUE
, если процесс успешно создан, и FALSE
, если произошла ошибка. В случае ошибки, вы можете использовать GetLastError
для получения кода ошибки и дальнейшей обработки.
Также обратите внимание на необходимость закрыть дескрипторы процесса и потока с помощью CloseHandle
после того, как они не нужны, чтобы избежать утечек ресурсов.
Функция CreateProcess
в Windows API имеет множество параметров для настройки создания нового процесса. Вот описание основных параметров:
lpApplicationName
(тип:LPCTSTR
):- Имя исполняемого файла, который нужно запустить. Этот параметр может быть NULL, если имя исполняемого файла включено в строку командной строки.
lpCommandLine
(тип:LPTSTR
):- Командная строка, передаваемая запускаемому процессу. Этот параметр может содержать имя исполняемого файла и его аргументы. Если
lpApplicationName
не является NULL, тоlpCommandLine
будет использоваться для передачи аргументов командной строки. В противном случае,lpCommandLine
должен содержать полную команду.
- Командная строка, передаваемая запускаемому процессу. Этот параметр может содержать имя исполняемого файла и его аргументы. Если
lpProcessAttributes
(тип:LPSECURITY_ATTRIBUTES
):- Атрибуты безопасности процесса. Обычно устанавливаются в NULL для использования атрибутов по умолчанию.
lpThreadAttributes
(тип:LPSECURITY_ATTRIBUTES
):- Атрибуты безопасности потока. Обычно устанавливаются в NULL для использования атрибутов по умолчанию.
bInheritHandles
(тип:BOOL
):- Флаг, указывающий, должны ли дескрипторы открытых файлов и другие ресурсы наследоваться процессом, созданным функцией
CreateProcess
.
- Флаг, указывающий, должны ли дескрипторы открытых файлов и другие ресурсы наследоваться процессом, созданным функцией
dwCreationFlags
(тип:DWORD
):- Флаги создания процесса, определяющие различные параметры и поведение процесса. Например, вы можете использовать
CREATE_NEW_CONSOLE
, чтобы создать новое окно консоли для процесса.
- Флаги создания процесса, определяющие различные параметры и поведение процесса. Например, вы можете использовать
lpEnvironment
(тип:LPVOID
):- Указатель на блок переменных окружения, которые будут использоваться в новом процессе. Обычно устанавливается в NULL, чтобы процесс унаследовал текущее окружение.
lpCurrentDirectory
(тип:LPCTSTR
):- Текущий рабочий каталог для нового процесса. Обычно устанавливается в NULL, чтобы процесс использовал текущий рабочий каталог родительского процесса.
lpStartupInfo
(тип:LPSTARTUPINFO
):- Указатель на структуру STARTUPINFO, которая содержит информацию о создаваемом процессе, такую как дескрипторы для ввода, вывода и ошибок.
lpProcessInformation
(тип:LPPROCESS_INFORMATION
):- Указатель на структуру PROCESS_INFORMATION, в которой будут возвращены дескрипторы процесса и потока после успешного создания процесса.
Информация о процессе
Для получения информации о процессе в операционной системе Windows с помощью Windows API (WinAPI) можно использовать несколько функций и структур данных. Вот основные средства для получения информации о процессе:
OpenProcess
:- Функция
OpenProcess
позволяет открыть существующий процесс и получить дескриптор процесса (HANDLE), который можно использовать для выполнения различных операций над процессом. Эта функция принимает в качестве параметров идентификатор процесса (PID) и права доступа.
- Функция
HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, processId);
GetProcessId
:- Функция
GetProcessId
используется для получения идентификатора процесса (PID) по его дескриптору.
- Функция
DWORD processId = GetProcessId(hProcess);
GetProcessTimes
:- Функция
GetProcessTimes
позволяет получить информацию о времени выполнения процесса, включая время начала выполнения и использования ЦП.
- Функция
FILETIME creationTime, exitTime, kernelTime, userTime;
if (GetProcessTimes(hProcess, &creationTime, &exitTime, &kernelTime, &userTime)) {
// Обработка информации о времени выполнения процесса
}
GetProcessMemoryInfo
:- Функция
GetProcessMemoryInfo
позволяет получить информацию о потреблении памяти процессом.
- Функция
PROCESS_MEMORY_COUNTERS memInfo;
if (GetProcessMemoryInfo(hProcess, &memInfo, sizeof(memInfo))) {
// Обработка информации о потреблении памяти процессом
}
QueryFullProcessImageName
:- Функция
QueryFullProcessImageName
используется для получения полного пути к исполняемому файлу процесса.
- Функция
TCHAR processPath[MAX_PATH];
DWORD pathSize = sizeof(processPath) / sizeof(TCHAR);
if (QueryFullProcessImageName(hProcess, 0, processPath, &pathSize)) {
// Обработка пути к исполняемому файлу процесса
}
CloseHandle
:- Для освобождения ресурсов, связанных с дескриптором процесса, используйте функцию
CloseHandle
.
- Для освобождения ресурсов, связанных с дескриптором процесса, используйте функцию
Обратите внимание, что в приведенных примерах hProcess
— это дескриптор открытого процесса, а processId
— идентификатор процесса. Важно учесть, что для успешного использования этих функций требуются соответствующие права доступа к процессу, и некоторые из них могут вернуть ошибки при попытке получения информации о некоторых процессах из-за ограничений безопасности.
Управление приоритетом процессов
Управление приоритетом процессов в операционной системе Windows можно выполнять с помощью Windows API (WinAPI). Приоритет процесса определяет, как операционная система распределяет процессорное время между процессами. Вот основные функции и концепции, связанные с управлением приоритетом процессов:
-
Установка приоритета процесса:
Для установки приоритета процесса используется функция
SetPriorityClass
. Эта функция изменяет приоритет выполнения всего процесса.BOOL success = SetPriorityClass(GetCurrentProcess(), priority);
Здесь
priority
может принимать одно из следующих значений:HIGH_PRIORITY_CLASS
: Высокий приоритет.NORMAL_PRIORITY_CLASS
: Нормальный приоритет (по умолчанию).IDLE_PRIORITY_CLASS
: Низкий приоритет.REALTIME_PRIORITY_CLASS
: Реальное время (осторожно при использовании, так как это может привести к зависанию системы).
-
Получение текущего приоритета процесса:
Для получения текущего приоритета процесса используется функция
GetPriorityClass
.DWORD priority = GetPriorityClass(GetCurrentProcess());
Значение
priority
будет одним из перечисленных выше констант. -
Установка приоритета потока:
Внутри процесса можно устанавливать приоритет для отдельных потоков с помощью функции
SetThreadPriority
. Это позволяет управлять приоритетами выполнения различных задач внутри одного процесса.BOOL success = SetThreadPriority(hThread, priority);
Здесь
hThread
— дескриптор потока, аpriority
— желаемый приоритет потока. -
Получение текущего приоритета потока:
Для получения текущего приоритета потока используется функция
GetThreadPriority
.int priority = GetThreadPriority(hThread);
Значение
priority
будет числовым представлением приоритета потока.
Обратите внимание, что изменение приоритетов процессов и потоков должно выполняться осторожно, так как неправильное управление приоритетами может повлиять на производительность системы. Особенно следует быть осторожным при изменении приоритета процессов в режиме реального времени, так как это может привести к проблемам с отзывчивостью системы.
Управление потоками внутри процесса
Управление потоками внутри процесса в операционной системе Windows можно выполнять с использованием Windows API (WinAPI). Вот основные функции и концепции, связанные с управлением потоками:
-
Создание потока:
Для создания нового потока внутри процесса используется функция
CreateThread
. Эта функция позволяет запустить новый поток и выполнить в нем определенную функцию.HANDLE hThread = CreateThread( NULL, // Атрибуты безопасности потока (обычно NULL) 0, // Размер стека (0 = размер стека по умолчанию) ThreadFunction, // Функция, которая будет выполнена в потоке lpParam, // Дополнительные параметры для функции 0, // Флаги создания потока (0 = запуск сразу после создания) &dwThreadId // Идентификатор потока );
Здесь
ThreadFunction
— это указатель на функцию, которая будет выполняться в потоке, иlpParam
— дополнительные параметры, которые могут быть переданы в функцию. -
Завершение потока:
Для завершения выполнения потока используется функция
ExitThread
. Вызов этой функции приведет к завершению текущего потока. -
Ожидание завершения потока:
Для ожидания завершения выполнения потока используется функция
WaitForSingleObject
илиWaitForMultipleObjects
, в зависимости от количества потоков, которые нужно ожидать.DWORD dwExitCode; DWORD dwWaitResult = WaitForSingleObject(hThread, INFINITE); if (dwWaitResult == WAIT_OBJECT_0) { // Поток завершил выполнение GetExitCodeThread(hThread, &dwExitCode); // Обработка результата выполнения потока (dwExitCode) } else { // Обработка ошибки ожидания }
-
Установка приоритета потока:
Установка приоритета выполнения потока внутри процесса выполняется с помощью функции
SetThreadPriority
.BOOL success = SetThreadPriority(hThread, priority);
Здесь
hThread
— дескриптор потока, аpriority
— желаемый приоритет потока. -
Получение текущего приоритета потока:
Для получения текущего приоритета потока используется функция
GetThreadPriority
.int priority = GetThreadPriority(hThread);
-
Закрытие дескриптора потока:
После завершения работы с потоком, его дескриптор должен быть закрыт с помощью функции
CloseHandle
.
Обратите внимание, что управление потоками требует аккуратности, так как неправильное использование потоков может привести к различным проблемам, таким как гонки данных и блокировки. Важно правильно синхронизировать доступ к общим ресурсам, к которым имеют доступ несколько потоков в вашем приложении.
Синхронизация между потоками и процессами
Синхронизация между потоками и процессами является важной частью многозадачных приложений, которые работают в операционной системе Windows. Она позволяет управлять доступом к общим ресурсам, обеспечивать правильный порядок выполнения задач и избегать гонок данных. Вот основные механизмы синхронизации, доступные в Windows API:
-
Критические секции (Critical Sections):
- Критические секции предоставляют простой и легковесный способ синхронизации между потоками внутри одного процесса.
- Функции для работы с критическими секциями включают
InitializeCriticalSection
,EnterCriticalSection
,LeaveCriticalSection
, иDeleteCriticalSection
.
-
Мьютексы (Mutexes):
- Мьютексы могут использоваться для синхронизации между разными процессами, а не только потоками внутри одного процесса.
- Функции для работы с мьютексами включают
CreateMutex
,WaitForSingleObject
, иReleaseMutex
.
-
Семафоры (Semaphores):
- Семафоры позволяют контролировать доступ к ресурсам, когда требуется счетчик.
- Функции для работы с семафорами включают
CreateSemaphore
,WaitForSingleObject
, иReleaseSemaphore
.
-
События (Events):
- События используются для уведомления одного или нескольких потоков или процессов о возникновении события.
- Функции для работы с событиями включают
CreateEvent
,SetEvent
,WaitForSingleObject
, и другие.
-
Критические ресурсы и мьютексы файла (File Mapping and File Mutexes):
- Эти механизмы позволяют синхронизировать доступ к разделяемым данным, которые находятся в памяти или на диске.
- Самые распространенные функции включают
CreateFileMapping
,MapViewOfFile
, иWaitForSingleObject
.
-
Readers-Writers Locks:
- Эти механизмы синхронизации позволяют определять правила доступа для читающих и записывающих потоков к общим данным.
- Функции для работы с Reader-Writer Locks включают
InitializeSRWLock
,AcquireSRWLockExclusive
, иAcquireSRWLockShared
.
-
События завершения (Completion Events):
- Эти события используются для уведомления о завершении выполнения асинхронных операций ввода-вывода.
- Функции для работы с событиями завершения включают
CreateIoCompletionPort
,GetQueuedCompletionStatus
, и другие.
Выбор подходящего механизма синхронизации зависит от конкретных требований вашего приложения и структуры данных. Важно правильно использовать синхронизацию, чтобы избежать проблем с гонками данных, блокировками и ожиданиями.
Завершение процессов
В Windows API (WinAPI) есть несколько способов завершения процессов, включая нормальное завершение и принудительное завершение. Вот основные функции и методы для завершения процессов:
-
ExitProcess:
- Функция
ExitProcess
используется для нормального завершения текущего процесса. Это завершает выполнение текущей программы и завершает процесс.
Где
exitCode
— код завершения процесса, который будет возвращен операционной системой. - Функция
-
TerminateProcess:
- Функция
TerminateProcess
используется для принудительного завершения другого процесса. Это позволяет завершить процесс, даже если он не отвечает или заблокирован.
BOOL success = TerminateProcess(hProcess, exitCode);
Где
hProcess
— дескриптор процесса, который нужно завершить, иexitCode
— код завершения процесса. - Функция
-
WM_CLOSE и PostMessage:
- Если вы хотите завершить приложение с графическим интерфейсом, вы можете отправить сообщение
WM_CLOSE
главному окну приложения с помощью функцииPostMessage
. Это позволит приложению выполнить закрытие как обычно.
PostMessage(hWnd, WM_CLOSE, 0, 0);
Где
hWnd
— дескриптор главного окна приложения. - Если вы хотите завершить приложение с графическим интерфейсом, вы можете отправить сообщение
Обратите внимание, что при использовании функции TerminateProcess
процесс завершается немедленно и не выполняет никаких завершающих операций, что может привести к утечкам ресурсов и несохранению данных. По возможности рекомендуется использовать нормальное завершение процессов с помощью ExitProcess
или закрытие окон с использованием WM_CLOSE
и PostMessage
, чтобы приложение могло выполнить необходимые действия перед завершением.
Получение информации о процессах
Для получения информации о процессах в операционной системе Windows можно использовать Windows API. Существует несколько функций и структур данных, которые позволяют получить информацию о текущих работающих процессах. Вот пример получения списка процессов и их атрибутов:
#include <windows.h>
#include <stdio.h>
#include <tlhelp32.h>
#include <tchar.h>
int main() {
SetConsoleOutputCP(1251);
// Создаем объект, представляющий снимок всех процессов
HANDLE hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hProcessSnap == INVALID_HANDLE_VALUE) {
// Обработка ошибки
return 1;
}
// Структура, в которую будет сохранен атрибут процесса
PROCESSENTRY32 pe32;
pe32.dwSize = sizeof(PROCESSENTRY32);
// Получаем информацию о первом процессе в снимке
if (!Process32First(hProcessSnap, &pe32)) {
CloseHandle(hProcessSnap);
// Обработка ошибки
return 1;
}
// Перебираем все процессы в снимке
do {
_tprintf(_T("Процесс ID: %d, Имя: %s\n"), pe32.th32ProcessID, pe32.szExeFile);
// Здесь можно получать и обрабатывать другие атрибуты процесса
} while (Process32Next(hProcessSnap, &pe32));
CloseHandle(hProcessSnap);
return 0;
}
Обратите внимание на следующие ключевые моменты в этом коде:
-
CreateToolhelp32Snapshot
создает снимок (snapshot) всех процессов в системе. -
Process32First
используется для получения информации о первом процессе в снимке, аProcess32Next
для перебора всех остальных процессов. -
PROCESSENTRY32
— это структура, которая хранит атрибуты процесса, такие как идентификатор процесса (PID) и имя исполняемого файла. -
Информация о процессе выводится на экран с помощью
_tprintf
, но ее можно использовать для любой нужной обработки.
Обратите внимание, что для успешной компиляции и выполнения этого кода необходимо включить библиотеку kernel32.lib
и указать верное символическое имя или путь к исполняемому файлу.
Модули процесса
Для получения информации о модулях (библиотеках и исполняемых файлах) внутри процесса в операционной системе Windows, вы можете использовать Windows API. Основным инструментом для этой задачи является функция EnumProcessModules
. Вот пример использования:
#include <windows.h>
#include <psapi.h>
#include <tchar.h>
#include <stdio.h>
int main() {
SetConsoleOutputCP(1251);
DWORD processId = GetCurrentProcessId(); // Идентификатор текущего процесса
HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, processId);
if (hProcess == NULL) {
// Обработка ошибки открытия процесса
return 1;
}
HMODULE hModules[1024];
DWORD cbNeeded;
// Получаем список модулей внутри процесса
if (EnumProcessModules(hProcess, hModules, sizeof(hModules), &cbNeeded)) {
for (DWORD i = 0; i < (cbNeeded / sizeof(HMODULE)); i++) {
TCHAR szModName[MAX_PATH];
// Получаем имя модуля
if (GetModuleFileNameEx(hProcess, hModules[i], szModName, sizeof(szModName) / sizeof(TCHAR))) {
_tprintf(_T("Модуль #%u: %s\n"), i, szModName);
}
}
}
CloseHandle(hProcess);
return 0;
}
В этом примере мы:
-
Получаем идентификатор текущего процесса с помощью
GetCurrentProcessId
. -
Открываем процесс с помощью
OpenProcess
, чтобы получить дескриптор процесса с правами на чтение информации о модулях. -
Используем
EnumProcessModules
для получения списка модулей внутри процесса.EnumProcessModules
возвращает массив дескрипторов модулей. -
Для каждого модуля мы используем
GetModuleFileNameEx
, чтобы получить полный путь к модулю и выводим его имя.
Не забудьте включить библиотеку psapi.lib
при компиляции и убедитесь, что код выполняется с правами, позволяющими открывать процессы и читать их модули.
Управление разрешениями и защитой процессов
Функции OpenProcessToken
и GetTokenInformation
в Windows API используются для получения информации о безопасности и разрешениях, связанных с процессом. Давайте рассмотрим их использование подробнее:
-
OpenProcessToken:
Функция
OpenProcessToken
используется для открытия дескриптора безопасности (токена) процесса. Этот токен содержит информацию о безопасности, такую как SID (идентификатор безопасности) пользователя и группы, разрешения и другие атрибуты безопасности процесса. Вот как можно использоватьOpenProcessToken
:#include <windows.h> #include <tchar.h> int main() { SetConsoleOutputCP(1251); DWORD processId = ...; // Идентификатор процесса, для которого нужно получить токен HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, processId); if (hProcess == NULL) { // Обработка ошибки return 1; } HANDLE hToken; if (OpenProcessToken(hProcess, TOKEN_QUERY, &hToken)) { // Теперь у вас есть дескриптор токена (hToken) для процесса, // который вы можете использовать для получения информации о безопасности. // Закрываем дескриптор токена после использования. CloseHandle(hToken); } CloseHandle(hProcess); return 0; }
-
GetTokenInformation:
После открытия дескриптора токена с помощью
OpenProcessToken
, вы можете использовать функциюGetTokenInformation
, чтобы получить различные атрибуты и информацию о безопасности. Вы должны предоставить буфер для хранения информации. Вот пример получения информации о SID (идентификаторе безопасности) пользователя из токена:HANDLE hToken = ...; // Дескриптор токена DWORD dwSize = 0; // Получаем размер буфера, необходимый для информации о SID. GetTokenInformation(hToken, TokenUser, NULL, 0, &dwSize); // Выделяем буфер и получаем информацию о SID. PTOKEN_USER pTokenUser = (PTOKEN_USER)malloc(dwSize); if (GetTokenInformation(hToken, TokenUser, pTokenUser, dwSize, &dwSize)) { // Теперь у вас есть информация о SID пользователя. // Освобождаем выделенный буфер после использования. free(pTokenUser); } // Закрываем дескриптор токена. CloseHandle(hToken);
Использование критических секций
Критические секции (Critical Sections) — это механизм синхронизации в Windows API, который позволяет защитить доступ к общим данным от одновременного доступа нескольких потоков внутри одного процесса. Они обычно используются для предотвращения гонок данных и обеспечения корректного доступа к разделяемым ресурсам. Вот пример использования критических секций:
#include <windows.h>
// Объявляем глобальную критическую секцию
CRITICAL_SECTION g_criticalSection;
// Функция, выполняемая в потоках
DWORD WINAPI ThreadFunction(LPVOID lpParam) {
// Входим в критическую секцию
EnterCriticalSection(&g_criticalSection);
// Здесь можно выполнять операции с общими данными
// Выходим из критической секции
LeaveCriticalSection(&g_criticalSection);
return 0;
}
int main() {
SetConsoleOutputCP(1251);
// Инициализируем критическую секцию
InitializeCriticalSection(&g_criticalSection);
// Создаем потоки, которые будут использовать критическую секцию
HANDLE hThread1 = CreateThread(NULL, 0, ThreadFunction, NULL, 0, NULL);
HANDLE hThread2 = CreateThread(NULL, 0, ThreadFunction, NULL, 0, NULL);
// Ожидаем завершения потоков
WaitForSingleObject(hThread1, INFINITE);
WaitForSingleObject(hThread2, INFINITE);
// Закрываем дескрипторы потоков
CloseHandle(hThread1);
CloseHandle(hThread2);
// Уничтожаем критическую секцию
DeleteCriticalSection(&g_criticalSection);
return 0;
}
Обратите внимание на следующие моменты:
-
Мы используем
InitializeCriticalSection
для инициализации критической секции иDeleteCriticalSection
для ее удаления после использования. -
В функции
ThreadFunction
перед доступом к общим данным мы входим в критическую секцию с помощьюEnterCriticalSection
, а после завершения операций с общими данными мы выходим из нее с помощьюLeaveCriticalSection
. -
Потоки создаются с помощью
CreateThread
, и каждый из них выполняетThreadFunction
. -
Мы ожидаем завершения выполнения потоков с помощью
WaitForSingleObject
.
Использование критических секций обычно предпочтительнее внутри одного процесса, так как они более эффективны и проще в использовании по сравнению с другими механизмами синхронизации, такими как мьютексы и семафоры. Критические секции обеспечивают внутреннюю синхронизацию в пределах одного процесса и не подходят для синхронизации между разными процессами.
Использование мьютексов
Мьютексы (Mutexes) — это механизм синхронизации в Windows API, который используется для управления доступом к разделяемым ресурсам, чтобы предотвратить гонки данных между несколькими потоками или процессами. Мьютексы обычно используются для синхронизации между потоками в разных процессах. Вот пример использования мьютексов для синхронизации между двумя потоками:
#include <windows.h>
#include <stdio.h>
// Объявляем глобальный мьютекс
HANDLE g_mutex;
// Функция, выполняемая в потоках
DWORD WINAPI ThreadFunction(LPVOID lpParam) {
// Попытка захвата мьютекса
DWORD dwWaitResult = WaitForSingleObject(g_mutex, INFINITE);
if (dwWaitResult == WAIT_OBJECT_0) {
// Мьютекс успешно захвачен
// Здесь можно выполнять операции с разделяемыми ресурсами
// Освобождение мьютекса
ReleaseMutex(g_mutex);
} else {
// Обработка ошибки
}
return 0;
}
int main() {
SetConsoleOutputCP(1251);
// Создаем мьютекс
g_mutex = CreateMutex(NULL, FALSE, NULL);
if (g_mutex == NULL) {
// Обработка ошибки создания мьютекса
return 1;
}
// Создаем два потока
HANDLE hThread1 = CreateThread(NULL, 0, ThreadFunction, NULL, 0, NULL);
HANDLE hThread2 = CreateThread(NULL, 0, ThreadFunction, NULL, 0, NULL);
// Ожидаем завершения потоков
WaitForSingleObject(hThread1, INFINITE);
WaitForSingleObject(hThread2, INFINITE);
// Закрываем дескрипторы потоков
CloseHandle(hThread1);
CloseHandle(hThread2);
// Закрываем дескриптор мьютекса
CloseHandle(g_mutex);
return 0;
}
Обратите внимание на следующие моменты:
-
Мы используем
CreateMutex
для создания мьютекса иCloseHandle
для его закрытия после использования. -
В функции
ThreadFunction
мы используемWaitForSingleObject
для попытки захвата мьютекса иReleaseMutex
для его освобождения после завершения операций с разделяемыми ресурсами. -
Мьютекс может быть захвачен только одним потоком или процессом одновременно. Если другой поток или процесс попытается захватить мьютекс, пока он уже занят, то он будет блокирован до освобождения мьютекса текущим владельцем.
Это простой пример использования мьютексов для синхронизации между двумя потоками. Мьютексы также могут использоваться для синхронизации между разными процессами, если они используют один и тот же мьютекс с именем, доступным для обоих процессов.
Использование семафоров
Семафоры (Semaphores) — это ещё один механизм синхронизации в Windows API, который используется для контроля доступа к разделяемым ресурсам между несколькими потоками или процессами. Семафоры могут позволить нескольким потокам одновременно получить доступ к общему ресурсу в ограниченном количестве. Вот пример использования семафора для синхронизации между несколькими потоками:
#include <windows.h>
#include <stdio.h>
// Объявляем глобальный семафор и устанавливаем начальное значение
HANDLE g_semaphore;
// Функция, выполняемая в потоках
DWORD WINAPI ThreadFunction(LPVOID lpParam) {
// Попытка уменьшить счетчик семафора
DWORD dwWaitResult = WaitForSingleObject(g_semaphore, INFINITE);
if (dwWaitResult == WAIT_OBJECT_0) {
// Семафор успешно уменьшен
// Здесь можно выполнять операции с разделяемыми ресурсами
// Увеличение счетчика семафора
ReleaseSemaphore(g_semaphore, 1, NULL);
} else {
// Обработка ошибки
}
return 0;
}
int main() {
SetConsoleOutputCP(1251);
// Создаем семафор с начальным счетчиком
g_semaphore = CreateSemaphore(NULL, 2, 2, NULL); // В данном примере, начальный счетчик равен 2
if (g_semaphore == NULL) {
// Обработка ошибки создания семафора
return 1;
}
// Создаем два потока
HANDLE hThread1 = CreateThread(NULL, 0, ThreadFunction, NULL, 0, NULL);
HANDLE hThread2 = CreateThread(NULL, 0, ThreadFunction, NULL, 0, NULL);
// Ожидаем завершения потоков
WaitForSingleObject(hThread1, INFINITE);
WaitForSingleObject(hThread2, INFINITE);
// Закрываем дескрипторы потоков
CloseHandle(hThread1);
CloseHandle(hThread2);
// Закрываем дескриптор семафора
CloseHandle(g_semaphore);
return 0;
}
Обратите внимание на следующие моменты:
-
Мы используем
CreateSemaphore
для создания семафора иCloseHandle
для его закрытия после использования. Начальное значение счетчика семафора в данном примере равно 2, что позволяет двум потокам одновременно получить доступ к разделяемым ресурсам. -
В функции
ThreadFunction
мы используемWaitForSingleObject
для попытки уменьшения счетчика семафора иReleaseSemaphore
для его увеличения после завершения операций с разделяемыми ресурсами. -
Семафор может позволить одновременно получить доступ только указанному количеству потоков (в данном случае, двум потокам). Если больше потоков попытаются одновременно уменьшить счетчик семафора, они будут блокированы до его освобождения другими потоками.
Семафоры предоставляют мощный механизм для управления доступом к разделяемым ресурсам в многопоточных и многопроцессных приложениях.
Использование событий
События (Events) — это механизм синхронизации в Windows API, который используется для уведомления одного или нескольких потоков о наступлении определенного события. События могут быть использованы для синхронизации между потоками или процессами, когда один поток ждет, пока другой поток или процесс оповестит его о наступлении события. Вот пример использования событий:
#include <windows.h>
#include <stdio.h>
// Объявляем глобальное событие
HANDLE g_event;
// Функция, выполняемая в потоках
DWORD WINAPI ThreadFunction(LPVOID lpParam) {
// Ожидание события
DWORD dwWaitResult = WaitForSingleObject(g_event, INFINITE);
if (dwWaitResult == WAIT_OBJECT_0) {
// Событие успешно сработало
// Здесь можно выполнять действия, связанные с событием
printf("Событие сработало в потоке.\n");
} else {
// Обработка ошибки
}
return 0;
}
int main() {
SetConsoleOutputCP(1251);
// Создаем событие
g_event = CreateEvent(NULL, FALSE, FALSE, NULL);
if (g_event == NULL) {
// Обработка ошибки создания события
return 1;
}
// Создаем поток
HANDLE hThread = CreateThread(NULL, 0, ThreadFunction, NULL, 0, NULL);
// Задержка для демонстрации события
Sleep(2000);
// Устанавливаем событие
SetEvent(g_event);
// Ожидаем завершения потока
WaitForSingleObject(hThread, INFINITE);
// Закрываем дескриптор потока
CloseHandle(hThread);
// Закрываем дескриптор события
CloseHandle(g_event);
return 0;
}
Обратите внимание на следующие моменты:
-
Мы используем
CreateEvent
для создания события иCloseHandle
для его закрытия после использования. В данном примере событие создается в несигнальном состоянии (сигнал равен FALSE), что означает, что ожидающие потоки или процессы будут блокированы до тех пор, пока событие не будет установлено. -
В функции
ThreadFunction
мы используемWaitForSingleObject
для ожидания события. Когда событие сработает (будет установлено), поток продолжит выполнение и выполнит действия, связанные с событием. -
В основной функции, после создания потока, мы устанавливаем событие с помощью
SetEvent
. Это приводит к тому, что ожидающий потокThreadFunction
сразу же продолжает выполнение.
События могут быть использованы для синхронизации и сигнализации между потоками или процессами, и они часто используются в многозадачных и многопроцессных приложениях для организации совместной работы потоков.