Windows для профессионалов

       

Программа-пример WaitForMultExp


Эта программа, "10 WaitForMultExp.exe" (см. листинг на рис. 10-4), предназначена для тестирования функции WaitForMultipleExpressions. Файлы исходного кода и ресурсов этой программы находятся в каталоге l0-WaitForMultExp на компакт-диске, прилагаемом к кпиге. После запуска WaitForMultExp открывается диалоговое окно, показанное ниже.

Если Вы нс станете изменять предлагаемые параметры, а просто щелкнете кнопку Wait For Multiple Expressions, диалоговое окно будет выглядеть так, как показано на следующей иллюстрации.

Программа создаст четыре объекта ядра "событие" в занятом состоянии и помещает в многоколоночный список (с возможностью выбора сразу нескольких элементов) по одной записи для каждого объекта ядра. Далее программа анализирует содержимое поля Expression и формирует массив описателей. По умолчанию я предлагаю объекты ядра и выражение, как в предыдущем примере.

Поскольку я задал время ожидания равным 30000 мс, у Вас есть 30 секунд на внесение изменений. Выбор элемента в нижнем списке приводит к вызову SetEvent, которая освобождает объект, а отказ от его выбора — к вызову ResetEvent и соответственно к переводу объекта в занятое состояние. После выбора достаточного числа элементов (удовлетворяющего одному из выражений) WaitForMultipleExpressions возвращает управление, и в нижней части диалогового окна показывается, какому выражению удовлетворяет Ваш выбор. Если Вы не уложитесь в 30 секунд, появится слово "Timeout".

Теперь обсудим мою функцию WaitForMultipleExpressions. Реализовать ее было не просто, и ее применение, конечно, приводит к некоторым издержкам. Как Вы знаете, в Windows есть функция WaitForMultipleObjects, которая позволяет потоку ждать по единственному AND-выражснию.

DWORD WaitForMultipleObjects( DWORD dwObjects, CONST HANDLE* pliObjects, BOOL fWaitAll, DWORD dwMimseconds);

Чтобы расширить ее функциональность для поддержки выражений, объединяемых OR, я должен создать несколько потоков — по одному на каждое такое выражение.
Каждый из этих потоков ждет в вызове WaitForMultipleObjectsEx по единственному AND-выражснию (Почему я использую эту функцию вместо более распространенной WaitForMultipleObjects - станет ясно позже). Когда какое-то выражение становится истинным, один из созданных потоков пробуждается и завершается.

Поток, который вызвал WaitForMultipleExpressions (и который породил все OR-пoтоки), должен ждать, пока одно из OR-выражений пе станет истинным. Для этого он вызывает функцию WaitForMultipleQbjeclsEx. В параметре dwObjects передается количество порожденных потоков (OR-выражений), а параметр phObjects указывает на массив описателей этих потоков. В параметр fWaitAll записывается FALSE, чтобы основной поток пробудился сразу после того, как станет истинным любое из выражений. И, наконец, в параметре dwMilliseconds передается значение, идентичное тому, которое было указано в аналогичном параметре при вызове WaitForMultipleExpressions.

Если в течение заданного времени ни одно из выражений не становится истинным, WaitForMultipleObjectsEx возвращает WAIT_TIMEOUT, и это же значение возвpaщается функцией WaitForMiltipleExpressions. А если какое-нибудь выражение становится истинным, WaitForMultipleObjectsEx возвращает индекс, указывающий, какой поток завершился. Так как каждый поток представляет отдельное выражение, этот индекс сообщает и то, какое выражение стало истинным; этот же индекс возвращается и функцией WaitForMultipleExpressions.



На этом мы, пожалуй, закончим рассмотрение того, как работает функция WaitForMultipleExpressions. Но нужно обсудить еще три вещи. Во-первых, нельзя допустить, чтобы несколько OR-потоков одновременно пробудились в своих вызовах WaitFor MultipleObjectsEx, так как успешное ожидание некоторых объектов ядра приводит к изменению их состояния (например, у семафора счетчик уменьшается на 1). WaitFor MultipleExpressions ждет лишь до тех пор, пока одно из выражений не станет истинным, а значит, я должен предотвратить более чем однократное изменение состояния объекта.



Решить эту проблему на самом деле довольно легко. Прежде чем порождать OR-потоки, я создаю собственный объект-семафор с начальным значением счетчика, равным 1. Далее каждый OR-поток вызывает WaitForMultipleObjectsEx и передает ей не только описатели объектов, связанных с выражением, но и описатель этого семафора. Теперь Вы понимаете, почему в каждом наборе не может быть более 63 описателей? Чтобы OR-поток пробудился, должны освободиться все объекты, которые он ждет, — в том числе мой специальный семафор. Поскольку начальное значение его счетчика равно 1, более одного OR-потока никогда не пробудится, и, следовательно, случайного изменения состояния каких-либо других объектов не произойдет.

Второе, на что нужно обратить внимание, - как заставить ждущий поток прекра тить ожидание для корректной очистки. Добавление семафора гарантирует, что пробудится не более чем один поток, но, раз мне уже известно, какое выражение стало истинным, я должен пробудить и остальные потоки, чтобы они корректно завершились. Вызова TerminateThread следует избегать, поэтому нужен какой-то другой механизм. Поразмыслив, я вспомнил, что потоки, ждущие в "тревожном" состоянии, принудительно пробуждаются, когда в АРС-очереди появляется какой-нибудь элемент.

Моя реализация WaitForMultipleExpressions для принудительного пробуждения по токов использует QueueUserAPC. После того как WaitForMultipleObjects, вызванная основным потоком, возвращает управление, я ставлю АРС-вызов в соответствующие очереди каждого из все еще ждущих OR-потоков:

// выводим все еще ждущие потоки из состояния сна,
// чтобы они могли корректно завершиться

for (dwExpNum = 0; dwExpNum < dwNumExps; dwExpNum++)
{

if ((WAIT_TIMEOUT == dwWaitRet) || (dwExpNum != (dwWaitRet - WAIT_OBJECT_0)))
{
QueueUserAPC(WFME_ExpressionAPC, ahThreads[dwExpNum], 0);
}

}

Функция обратного вызова, WFMEExpressionAPC, выглядит столь странно потому, что на самом деле от нее не требуется ничего, кроме одного: прервать ожидание потока.



// это АРС-функция обратного вызова

VOID WINAPI WFHE_ExpressionAPC(DWORD dwData}
{

// в тело функции преднамеренно не включено никаких операторов

}

Третье (и последнее) — правильная обработка интервалов ожидания. Если ника кие выражения так и не стали истинными в течение заданного времени, функция WaitForMultipleObjects, вызванная основным потоком, возвращает WAIT_TIMEOUT. В этом случае я должен позаботиться о том, чтобы ни одно выражение больше не ста ло бы истинным и тем самым не изменило бы состояние объектов. За это отвечает следующий код:

// ждем, когда выражение станет TRUE или когда истечет срок ожидания
dwWaitRet = WaitForMultiplcObjects(dwExpNum, ahThreads, FALSE, dwMilliseconds);

if (WAIT_TIMEOUT == dwWaitRet)
{

// срок ожидания истек, выясняем, не стало ли какое-нибудь выражение
// истинным, проверяя состояние семафора hsemOnlyOne
dwWaitRet = WaitForSingleObject(hsemOnlyOne, 0);

if (WAIT_TIMEOUT == dwWaitRet}
{
// если семафор не был переведен в свободное состояние,
// какое-то выражение дало TRUE, надо выяснить - какое
dwWaitRet = WaitForMultipleObjects(dwExpNum, ahThreads, FALSE. INFINITE);
}
else
{
// ни одно выражение не стало TRUE,
// и WaitForSingleObject просто отдала нам семафор
dwWaitRet = WAIT_TIMbOUT;
}
}

Я не даю другим выражениям стать истинными за счет ожидания на семафоре. Это приводит к уменьшению счетчика семафора до 0, и никакой OR-поток не может пробудиться. Но где-то после вызова функции WaitForMultipleObjects из основного потока и обращения той к WaitForSingleObject одно из выражений может стать истинным. Вот почему я проверяю значение, возвращаемое WaitForSingleQbject. Если она возвра щает WAIT_OBJECT_0, значит, семафор захвачен основным потоком и ни одно из выражений не стало истинным. Но если она возвращает WAIT_TIMEOUT, какое-то выражение все же стало истинным, прежде чем основной поюк успел захватить семафор. Чтобы выяснить, какое именно выражение дало TRUE, основной поток снова вызывает WaitForMultipleObjects, но уже с временем ожидания, равным INFINITE; здесь все в порядке, так как я знаю, что семафор захвачен OR-потоком и этот поток вот-вот завершится.


Теперь я должен пробудить остальные OR-потоки, чтобы корректно завершить их. Это делается в цикле, из которого вызывается QueueUserAPC (о ней я уже рассказывал).

Поскольку реализация WaitForMultipleExpressions основана на использовании группы потоков, каждый из которых ждет на своем наборе объектов, объединяемых по AND, мьютексы в ней неприменимы. В отличие от остальных объектов ядра мьютексы могут передаваться потоку во владение. Значит, если какой-нибудь из моих AND потоков заполучит мьютекс, то по его завершении произойдет отказ от мьютекса. Вот когда Microsoft добавит в Windows API функцию, позволяющую одному потоку передавать права на владение мьютексом другому потоку, тогда моя функция WaitForMultipleExpressions и сможет поддерживать мьютексы. А пока надежного и корректного способа ввести в WaitForMultipleExpressions такую поддержку я не вижу.



WaitForMultExp



Содержание раздела