Синхронизация процессов при работе с Windows (статья)
Синхронизация процессов при работе с Windows
Задача синхронизации встает при одновременном доступе нескольких процессов (или нескольких потоков одного процесса) к какому-либо ресурсу. Поскольку поток в Win32 может быть остановлен в любой, заранее ему неизвестный момент времени возможна ситуация, когда один из потоков не успел завершить модификацию ресурса (например, отображенной на файл области памяти), но был остановлен, а другой поток попытался обратиться к этому же ресурсу. В этот момент ресурс находится в несогласованном состоянии, и последствия обращения к нему могут быть самыми неожиданными – от порчи данных, до нарушения защиты памяти.
Главной идеей, положенной в основу синхронизации потоков в Win32 является использование объектов синхронизации и функций ожидания. Объекты могут находиться в одном из двух состояний – Signaled или Not Signaled. Функции ожидания блокируют выполнение потока до тех пор, пока заданный объект находится в состоянии Not Signaled. Таким образом, поток, которому необходим эксклюзивный доступ к ресурсу, должен выставить какой-либо объект синхронизации в несигнальное состояние, а по окончании – сбросить его в сигнальное. Остальные потоки должны перед доступом к этому ресурсу вызвать функцию ожидания, которая позволит им дождаться освобождения ресурса.
Рассмотрим, какие объекты и функции синхронизации предоставляет нам Win32 API.
Функции синхронизацииФункции синхронизации делятся на две основные категории – это функции, ожидающие единственного объекта и функции, ожидающие одного из нескольких объектов
Функции, ожидающие единственного объектаПростейшей функцией ожидания является
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;
В случае, когда требуется одновременно с ожиданием объекта, перевести в сигнальное состояние другой объект может использоваться функция:
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), либо использование функции:
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 и позволять прервать ожидание для обработки сообщений. Алгоритм аналогичен вышеприведенному примеру.
Прерывание ожидания по запросу на завершение операции ввода-вывода или APCWindows поддерживает асинхронные вызовы процедур. При создании каждого потока (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 бывает
Отключаемый вручную Будучи установленным в сигнальное состояние, остается в нем до тех пор, пока не будет переключен явным вызовом функции ResetEvent
Автоматически отключаемый Автоматически переключается в несигнальное состояние операционной системой, когда один из ожидающих его потоков завершается.
Для создания объекта используется функция:
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 воспользоваться функцией:
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
Устанавливает объект в сигнальное состояние, дает отработать всем функциям ожидания, ожидающим этот объект, а затем снова сбрасывает его.
В WinAPI события используются, для выполнения операций асинхронного ввода-вывода. Следующий пример показывает, как приложение инициирует запись одновременно в два файла, а затем ожидает завершения записи перед продолжением работы. Такой подход может обеспечить более высокую производительность при высокой интенсивности ввода-вывода, чем последовательная запись.
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).
Если мутекс уже существует, приложение может получить его идентификатор функцией
function OpenMutex(
dwDesiredAccess: DWORD; // Задает права доступа к объекту
bInheritHandle: BOOL; // Задает, может ли объект наследоваться
// дочерними процессами
lpName: PChar // Имя объекта
): THandle; stdcall;
Параметр dwDesiredAccess может принимать одно из следующих значений
MUTEX_ALL_ACCESS Приложение получает полный доступ к объекту
SYNCHRONIZE Только для Windows NT – приложение может использовать объект только в функциях ожидания и функции ReleaseMutex
Функция возвращает идентификатор открытого мутекса, либо 0, в случае ошибки. Мутекс переходит в сигнальное состояние после срабатывания функции ожидания, в которую был передан его идентификатор. Для возврата в несигнальное состояние служит функция
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 он переходит в несигнальное состояние, при любых других значениях счетчика – его состояние сигнальное. Такое поведение позволяет использовать семафор в качестве ограничителя доступа к ресурсу, поддерживающему заранее заданное количество подключений.
Для создания семафора служит функция:
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.
Идентификатор ранее созданного семафора может быть, также, получен функцией:
function OpenSemaphore(
dwDesiredAccess: DWORD; // Задает права доступа к объекту
bInheritHandle: BOOL; // Задает, может ли объект наследоваться
// дочерними процессами
lpName: PChar // Имя объекта
): THandle; stdcall;
Параметр dwDesiredAccess может принимать одно из следующих значений:
SEMAPHORE_ALL_ACCESS Поток получает все права на семафор
SEMAPHORE_MODIFY_STATE Поток может увеличивать счетчик семафора функцией ReleaseSemaphore
SYNCHRONIZE Только Windows NT – поток может использовать семафор в функциях ожидания
Для увеличения счетчика семафора используется функция:
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;
Waitable timer (таймер ожидания)
Таймер ожидания отсутствует в Windows 95 и для его использования необходима Windows 98 или Windows NT 4.0 и выше.
Таймер ожидания переходит в сигнальное состояние по завершении заданного интервала времени. Для его создания используется функция:
function CreateWaitableTimer(
lpTimerAttributes: PSecurityAttributes; // Адрес структуры
// TSecurityAttributes
bManualReset: BOOL; // Задает, будет ли таймер переходить в
// сигнальное состояние по завершении функции
// ожидания
lpTimerName: PChar // Имя объекта
): THandle; stdcall;
Если параметр bManualReset равен TRUE, то таймер после срабатывания функции ожидания остается в сигнальном состоянии до явного вызова SetWaitableTimer, если FALSE - таймер автоматически переходит в несигнальное состояние.
Если lpTimerName совпадает с именем уже существующего в системе таймера – функция возвращает его идентификатор, позволяя использовать объект для синхронизации между процессами. Имя таймера не должно совпадать с именем уже существующих объектов типов event, semaphore, mutex, job или file-mapping.
Идентификатор уже существующего таймера можно получить функцией:
function OpenWaitableTimer(
dwDesiredAccess: DWORD; // Задает права доступа к объекту
bInheritHandle: BOOL; // Задает, может ли объект наследоваться
// дочерними процессами
lpTimerName: PChar // Имя объекта
): THandle; stdcall;
Параметр dwDesiredAccess может принимать следующие значения:
TIMER_ALL_ACCESS Разрешает полный доступ к объекту
TIMER_MODIFY_STATE Разрешает изменять состояние таймера функциями SetWaitableTimer и CancelWaitableTimer
SYNCHRONIZE Только Windows NT – разрешает использовать таймер в функциях ожидания
После получения идентификатора таймера, поток может задать время его срабатывания функцией
function SetWaitableTimer(
hTimer: THandle; // Идентификатор таймера
const lpDueTime: TLargeInteger; // Время срабатывания
lPeriod: Longint; // Период повторения срабатывания
pfnCompletionRoutine: TFNTimerAPCRoutine; // Процедура-обработчик
lpArgToCompletionRoutine: Pointer;// Параметр процедуры-обработчика
fResume: BOOL // Задает, будет ли операционная
// система «пробуждаться»
): BOOL; stdcall;
Рассмотрим параметры подробнее.
lpDueTime
Задает время срабатывания таймера. Время задается в формате TFileTime и базируется на coordinated universal time (UTC), т.е. должно указываться по Гринвичу. Для преобразования системного времени в TFileTime используется функция SystemTimeToFileTime. Если время имеет положительный знак, оно трактуется как абсолютное, если отрицательный – как относительное от момента запуска таймера.
lPeriod
Задает срок между повторными срабатываниями таймера. Если lPeriod равен 0 – таймер сработает один раз.
pfnCompletionRoutine
Адрес функции, объявленной как:
procedure TimerAPCProc(
lpArgToCompletionRoutine: Pointer; // данные
dwTimerLowValue: DWORD; // младшие 32 разряда значения таймера
dwTimerHighValue: DWORD; // старшие 32 разряда значения таймера
); stdcall;
Эта функция вызывается, когда срабатывает таймер, если поток, ожидающий его срабатывания, использует функцию ожидания, поддерживающую асинхронный вызов процедур. В функцию передаются 3 параметра:
• | lpArgToCompletionRoutine – значение, переданное в качестве одноименного параметра в функцию SetWaitableTimer. Приложение может использовать его для передачи в процедуру обработки адреса блока данных, необходимых для её работы |
• | dwTimerLowValue и dwTimerHighValue – соответственно члены dwLowDateTime и dwHighDateTime структуры TFileTime. Они описывают время срабатывания таймера. Время задается в UTC формате (по Гринвичу). |
Если дополнительная функция обработки не нужна, в качестве этого параметра можно передать NIL.
lpArgToCompletionRoutine
Это значение передается в функцию pfnCompletionRoutine при её вызове.
fResume
Определяет необходимость "пробуждения" системы, если на момент срабатывания таймера она находится в режиме экономии электроэнергии (suspended). Если операционная система не поддерживает пробуждение и fResume равно TRUE, функция SetWaitableTimer выполнится успешно, однако последующий вызов GetLastError вернет результат ERROR_NOT_SUPPORTED.
Если необходимо перевести таймер в неактивное состояние, это можно сделать функцией:
function CancelWaitableTimer(hTimer: THandle): BOOL; stdcall;
Эта функция не изменяет состояния таймера и не приводит к срабатыванию функций ожидания и вызову процедур-обработчиков.
По завершении работы объект должен быть уничтожен функцией CloseHandle
Создадим класс, который ожидает в отдельном потоке наступления заданного времени, а затем вызывает процедуру главного потока приложения. Такой класс может использоваться, например, в планировщике заданий. Поскольку таймер ожидания позволяет задавать время срабатывания в абсолютных величинах, отпадает необходимость постоянно анализировать текущее время, используя обычный таймер Windows.
unit WaitThread; interface uses Classes, Windows; type TWaitThread = class(TThread) WaitUntil: TDateTime; procedure Execute; override; end; implementation uses SysUtils; procedure TWaitThread.Execute; var Timer: THandle; SystemTime: TSystemTime; FileTime, LocalFileTime: TFileTime; begin Timer := CreateWaitableTimer(NIL, FALSE, NIL); try DateTimeToSystemTime(WaitUntil, SystemTime); SystemTimeToFileTime(SystemTime, LocalFileTime); LocalFileTimeToFileTime(LocalFileTime, FileTime); SetWaitableTimer(Timer, TLargeInteger(FileTime), 0, NIL, NIL, FALSE); WaitForSingleObject(Timer, INFINITE); finally CloseHandle(Timer); end; end; end. Использовать этот класс можно, например, следующим образом: type TForm1 = class(TForm) Button1: TButton; procedure Button1Click(Sender: TObject); private procedure TimerFired(Sender: TObject); end; ... implementation uses WaitThread; procedure TForm1.Button1Click(Sender: TObject); var T: TDateTime; begin with TWaitThread.Create(TRUE) do begin OnTerminate := TimerFired; FreeOnTerminate := TRUE; // Срок ожидания закончится через 5 секунд WaitUntil := Now + 1 / 24 / 60 / 60 * 5; Resume; end; end; procedure TForm1.TimerFired(Sender: TObject); begin ShowMessage('Timer fired !'); end;Дополнительные объекты синхронизации
Некоторые объекты Win32 API не предназначены исключительно для целей синхронизации, однако могут использоваться с функциями синхронизации. Такими объектами являются:
Сообщение об изменении папки (change notification)
Windows позволяет организовать слежение за изменениями объектов файловой системы. Для этого служит функция
function FindFirstChangeNotification(
lpPathName: PChar; // Путь к папке, изменения в которой нас
// интересуют
bWatchSubtree: BOOL; // Задает необходимость слежения за
// изменениями во вложенных папках
dwNotifyFilter: DWORD // Фильтр событий
): THandle; stdcall;
Параметр dwNotifyFilter – это битовая маска из одного или нескольких следующих значений:
FILE_NOTIFY_CHANGE_FILE_NAME
Слежение ведется за любым изменением имени файла, в т.ч. созданием и удалением файлов
FILE_NOTIFY_CHANGE_DIR_NAME
Слежение ведется за любым изменением имени папки, в т.ч. созданием и удалением папок
FILE_NOTIFY_CHANGE_ATTRIBUTES
Слежение ведется за любым изменением аттрибутов
FILE_NOTIFY_CHANGE_SIZE
Слежение ведется за изменением размера файлов. Изменение размера происходит при записи в файл. Функция ожидания срабатывает только после успешного сброса дискового кэша
FILE_NOTIFY_CHANGE_LAST_WRITE
Слежение за изменением времени последней записи в файл, т.е., фактически, за любой записью в файл. Функция ожидания срабатывает только после успешного сброса дискового кэша.
FILE_NOTIFY_CHANGE_SECURITY
Слежение за любыми изменениями дескрипторов защиты
Идентификатор, возвращенный этой функцией, может использоваться в любой функции ожидания. Он переходит в сигнальное состояние, когда в папке происходят запрошенные для слежения изменения. Продолжить слежение можно, используя функцию:
function FindNextChangeNotification(
hChangeHandle: THandle
): BOOL; stdcall;
По завершении работы, идентификатор должен быть закрыт при помощи функции:
function FindCloseChangeNotification(
hChangeHandle: THandle
): BOOL; stdcall;
Чтобы не блокировать исполнение основного потока программы функцией ожидания, удобно реализовать ожидание изменений в отдельном потоке. Реализуем поток на базе класса TThread. Для того чтобы можно было прервать исполнение потока методом Terminate необходимо, чтобы функция ожидания, реализованная в методе Execute, также прерывалась при вызове Terminate. Для этого будем использовать вместо WaitForSingleObject функцию WaitForMultipleObjects, и прерывать ожидание по событию (event), устанавливаемому в Terminate.
type TCheckFolder = class(TThread) private FOnChange: TNotifyEvent; Handles: array[0..1] of THandle; // Идентификаторы объектов // синхронизации procedure DoOnChange; protected procedure Execute; override; public constructor Create(CreateSuspended: Boolean; PathToMonitor: String; WaitSubTree: Boolean; OnChange: TNotifyEvent; NotifyFilter: DWORD); destructor Destroy; override; procedure Terminate; end; procedure TCheckFolder.DoOnChange; // Эта процедура вызывается в контексте главного потока приложения // В ней можно использовать вызовы VCL, изменять состояние формы, // например перечитать содержимое TListBox, отображающего файлы begin if Assigned(FOnChange) then FOnChange(Self); end; procedure TCheckFolder.Terminate; begin inherited; // Вызываем TThread.Terminate, устанавливаем // Terminated = TRUE SetEvent(Handles[1]); // Сигнализируем о необходимости // прервать ожидание end; constructor TCheckFolder.Create(CreateSuspended: Boolean; PathToMonitor: String; WaitSubTree: Boolean; OnChange: TNotifyEvent; NotifyFilter: DWORD); var BoolForWin95: Integer; begin // Создаем поток остановленным inherited Create(TRUE); // Windows 95 содержит не очень корректную реализацию функции // FindFirstChangeNotification. Для корректной работы, необходимо, // чтобы: // - lpPathName - не содержал завершающего слэша "\" для // некорневого каталога // - bWatchSubtree - TRUE должен передаваться как BOOL(1) if WaitSubTree then BoolForWin95 := 1 else BoolForWin95 := 0; if (Length(PathToMonitor) > 1) and (PathToMonitor[Length(PathToMonitor)] = '\') and (PathToMonitor[Length(PathToMonitor)-1] <> ':') then Delete(PathToMonitor, Length(PathToMonitor), 1); Handles[0] := FindFirstChangeNotification( PChar(PathToMonitor), BOOL(BoolForWin95), NotifyFilter); Handles[1] := CreateEvent(NIL, TRUE, FALSE, NIL); FOnChange := OnChange; // И, при необходимости, запускаем if not CreateSuspended then Resume; end; destructor TCheckFolder.Destroy; begin FindCloseChangeNotification(Handles[0]); CloseHandle(Handles[1]); inherited; end; procedure TCheckFolder.Execute; var Reason: Integer; Dummy: Integer; begin repeat // Ожидаем изменения в папке, либо сигнала о завершении // потока Reason := WaitForMultipleObjects(2, @Handles, FALSE, INFINITE); if Reason = WAIT_OBJECT_0 then begin // Изменилась папка, вызываем обработчик в контексте // главного потока приложения Synchronize(DoOnChange); // И продолжаем поиск FindNextChangeNotification(Handles[0]); end; until Terminated; end;
Поскольку метод TThread.Terminate не виртуальный, этот класс нельзя использовать с переменной типа TThread, т.к. в этом случае будет вызываться Terminate от TThread, который не может прервать ожидания, и поток будет выполняться до изменения в папке, за которой ведется слежение.
Устройство стандартного ввода с консоли (console input)
Идентификатор, стандартного устройства ввода с консоли, полученный при помощи вызова функции GetStdHandle(STD_INPUT_HANDLE), можно использовать в функциях ожидания. Он находится в сигнальном состоянии, если очередь ввода консоли непустая и в несигнальном, если пустая. Это позволяет организовать ожидание ввода символов, либо, при помощи функции WaitForMultipleObjects совместить его с ожиданием каких-то других событий.
Задание (Job)
Job – это новый механизм Windows 2000, позволяющий объединить группу процессов в одно задание и манипулировать ими одновременно. Идентификатор задания находится в сигнальном состоянии, если все процессы, ассоциированные с ним завершились по причине истечения лимита времени на выполнение задания.
Процесс (Process)
Идентификатор процесса, полученный при помощи функции CreateProcess, переходит в сигнальное состояние по завершении процесса. Это позволяет организовать ожидание завершения процесса, например, при запуске из приложения внешней программы.
var
PI: TProcessInformation;
SI: TStartupInfo;
...
FillChar(SI, SizeOf(SI), 0);
SI.cb := SizeOf(SI);
Win32Check(CreateProcess(NIL, 'COMMAND.COM', NIL,
NIL, FALSE, 0, NIL, NIL, SI, PI));
// Задерживаем исполнение программы до завершения процесса
WaitForSingleObject(PI.hProcess, INFINITE);
CloseHandle(PI.hProcess);
CloseHandle(PI.hThread);
Следует понимать, что в этом случае вызывающий процесс будет заморожен полностью и не сможет обрабатывать сообщения. Поэтому, если дочерний процесс может выполняться в течение длительного времени, лучше использовать более корректный вариант ожидания, описанный в разделе, посвященном функции MsgWaitForMultipleObjects.
Поток (thread)
Идентификатор потока находится в несигнальном состоянии до тех пор, пока поток выполняется. По его завершении идентификатор переходит в сигнальное состояние. Это позволяет легко узнать, завершился ли поток, либо при помощи функции, ожидающей нескольких объектов, организовать ожидание завершения одного из, либо всех интересующих потоков.
Дополнительные механизмы синхронизацииКритические секции
Критические секции – это механизм, предназначенный для синхронизации потоков внутри одного процесса. Как и мутекс, критическая секция может в один момент времени принадлежать только одному потоку, однако, она предоставляет более быстрый и эффективный механизм, чем мутексы. Перед использованием критической секции необходимо инициализировать её функцией:
procedure InitializeCriticalSection(
var lpCriticalSection: TRTLCriticalSection
); stdcall;
После создания объекта поток, перед доступом к защищаемому ресурсу должен вызвать функцию:
procedure EnterCriticalSection(
var lpCriticalSection: TRTLCriticalSection
); stdcall;
Если в этот момент ни один из потоков в процессе не владеет объектом, то поток становится владельцем критической секции и продолжает выполнение. Если секция уже захвачена другим потоком то выполнение потока, вызвавшего функцию приостанавливается до её освобождения.
Поток, владеющий критической секцией, может повторно вызывать функцию EnterCriticalSection без блокирования своего исполнения. По завершению работы с защищаемым ресурсом поток должен вызвать функцию:
procedure LeaveCriticalSection(
var lpCriticalSection: TRTLCriticalSection
); stdcall;
Эта функция освобождает объект независимо от количества предыдущих вызовов потоком функции EnterCriticalSection. Если имеются другие потоки, ожидающие освобождения секции, один из них становится её владельцем и продолжает исполнение. Если поток завершился, не освободив критическую секцию, её состояние становится неопределенным, что может вызвать блокировку работы программы.
Имеется возможность попытаться захватить объект без замораживания потока. Для этого служит функция:
function TryEnterCriticalSection(
var lpCriticalSection: TRTLCriticalSection
): BOOL; stdcall;
Она проверяет, захвачена секция ли в момент её вызова. Если да – функция возвращает FALSE, в противном случае – захватывает секцию и возвращает TRUE.
По завершении работы с критической секцией, она должна быть уничтожена вызовом функции:
procedure DeleteCriticalSection(
var lpCriticalSection: TRTLCriticalSection
); stdcall;
Рассмотрим пример приложения, осуществляющего в нескольких потоках загрузку данных по сети. Глобальные переменные BytesSummary и TimeSummary хранят общее количество загруженных байт и время загрузки. Эти переменные каждый поток обновляет по мере считывания данных. Для предотвращения конфликтов приложение должно защитить общий ресурс при помощи критической секции.
var // Глобальные переменные CriticalSection: TRTLCriticalSection; BytesSummary: Cardinal; TimeSummary: TDateTime; AverageSpeed: Float; ... // При инициализации приложения InitializeCriticalSection(CriticalSection); BytesSummary := 0; TimeSummary := 0; AverageSpeed := 0; //В методе Execute потока, загружающего данные. repeat BytesRead := ReadDataBlockFromNetwork; EnterCriticalSection(CriticalSection); try BytesSummary := BytesSummary + BytesRead; TimeSummary := TimeSummary + (Now - ThreadStartTime); if TimeSummary > 0 then AverageSpeed := BytesSummary / (TimeSummary/24/60/60); finally LeaveCriticalSection(CriticalSection) end; until LoadComplete; // При завершении приложения DeleteCriticalSection(CriticalSection);
Delphi предоставляет класс, инкапсулирующий функциональность критической секции. Класс объявлен в модуле SyncObjs.pas
type
TCriticalSection = class(TSynchroObject)
public
constructor Create;
destructor Destroy; override;
procedure Acquire; override;
procedure Release; override;
procedure Enter;
procedure Leave;
end;
Методы Enter и Leave являются синонимами методов Acquire и Release соответственно и добавлены для лучшей читаемости исходного кода.
procedure TCriticalSection.Enter;
begin
Acquire;
end;
procedure TCriticalSection.Leave;
begin
Release;
end;
Защищенный доступ к переменным (Interlocked Variable Access)
Часто возникает необходимость совершения операций над разделяемыми между потоками 32-разрядными переменными. Для упрощения решения этой задачи WinAPI предоставляет функции для защищенного доступа к ним, не требующие использования дополнительных (и более сложных) механизмов синхронизации. Переменные, используемые в этих функциях, должны быть выровнены на границу 32-разрядного слова. Применительно к Delphi это означает, что если переменная объявлена внутри записи (record), то эта запись не должна быть упакованной (packed) и при её объявлении должна быть активна директива компилятора {$A+}. Несоблюдение этого требования может привести к возникновению ошибок на многопроцессорных конфигурациях.
type TPackedRecord = packed record A: Byte; B: Integer; end; // TPackedRecord.B нельзя использовать в функциях InterlockedXXX TNotPackedRecord = record A: Byte; B: Integer; end; {$A-} var A1: TNotPackedRecord; // A1.B нельзя использовать в функциях InterlockedXXX I: Integer // I можно использовать в функциях InterlockedXXX, т.к. переменные в // Delphi всегда выравниваются на границу слова безотносительно // к состоянию директивы компилятора $A {$A+} var A2: TNotPackedRecord; // A2.B можно использовать в функциях InterlockedXXX function InterlockedIncrement( var Addend: Integer ): Integer; stdcall;
Функция увеличивает переменную Addend на 1. Возвращаемое значение зависит от операционной системы:
Windows 98, Windows NT 4.0 и старше
Возвращается новое значение переменной Addend
Windows 95, Windows NT 3.51
Если после изменения Addend < 0 возвращается отрицательное число, не обязательно равное Addend Если Addend = 0 – возвращается 0 Если после изменения Addend > 0 возвращается положительное число, не обязательно равное Addend.
function InterlockedDecrement(
var Addend: Integer
): Integer; stdcall;
Функция уменьшает переменную Addend на 1. Возвращаемое значение аналогично функции InterlockedIncrement.
function InterlockedExchange(
var Target: Integer;
Value: Integer
): Integer; stdcall;
Функция записывает в переменную Target значение Value и возвращает предыдущее значение Target
Следующие функции для выполнения требуют Windows 98 или Windows NT 4.0 и старше.
function InterlockedCompareExchange(
var Destination: Pointer;
Exchange: Pointer;
Comperand: Pointer
): Pointer; stdcall;
Функция сравнивает значения Destination и Comperand. Если они совпадают, значение Exchange записывается в Destination. Функция возвращает начальное значение Destination.
function InterlockedExchangeAdd(
Addend: PLongint;
Value: Longint
): Longint; stdcall;
Функция добавляет к переменной, на которую указывает Addend значение Value и возвращает начальное значение Addend.
РезюмеМногозадачная и многопоточная среда Win32 предоставляет широкие возможности для написания высокоэффективных приложений. Однако, написание приложений, использующих многопоточность и взаимодействующих друг с другом, при неаккуратном программировании может привести к их неверной работе, неоправданной загрузке и даже блокировке всей системы. Во избежание этого следуйте нижеприведенным рекомендациям:
• | Если приложения или потоки одного процесса изменяют общий ресурс – защищайте доступ к нему при помощи критических секций или мутексов. |
• | Если доступ осуществляется только на чтение – защищать ресурс не обязательно. |
• | Критические секции более эффективны, но применимы только внутри одного процесса, мутексы могут использоваться для синхронизации между процессами. |
• | Используйте семафоры для ограничения количества обращений к одному ресурсу. |
• | Используйте события (event) для информирования потока о наступлении какого-либо события. |
• | Если разделяемый ресурс – 32-битная переменная – для синхронизации доступа к нему можно использовать функции, обеспечивающие разделяемый доступ к переменным. |
• | Многие объекты Win32 позволяют организовать эффективное слежение за своим состоянием при помощи функций ожидания. Это наиболее эффективный с точки зрения расхода системных ресурсов метод. |
• | Если Ваш поток создает (даже неявно, при помощи CoInitialize или функций DDE) окна – он должен обрабатывать сообщения. Не используйте в таком потоке функций не позволяющих прервать ожидание по приходу сообщения с большим или неограниченным периодом ожидания. Используйте функции MsgWaitForXXX |
Тенцер А. Л.
ICQ UIN 15925834
tolik@katren.nsk.ru