Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Создание эффективных приложений для Windows Джеффри Рихтер 2004 (Книга).pdf
Скачиваний:
377
Добавлен:
15.06.2014
Размер:
8.44 Mб
Скачать

объект hAutoResetEvent2 по-прежнему занят. Поскольку потоки все еще ждут, ни какого побочного эффекта для объекта hAutoResetEvent1 не возникает.

Наконец освобождается и объект hAutoResetEvent2 В этот момент один из пото ков обнаруживает, что освободились оба объекта, которых он ждал. Его ожидание успешно завершается, оба объекта снова переводятся в занятое состояние, и выпол нение потока возобновляется. А что же происходит со вторым потоком? Он продол жает ждать и будет делать это, пока вновь не освободятся оба объекта-события.

Как я уже упоминал, WaitForMiltipleObjects работает на уровне атомарного досту па, и это очень важно. Когда она проверяет состояние объектов ядра, никто не может «у нее за спиной» изменить состояние одного из этих объектов. Благодаря этому ис ключаются ситуации со взаимной блокировкой. Только представьте, что получится, если один из потоков, обнаружив освобождение hAutoResetEventl, сбросит его в заня тое состояние, а другой поток, узнав об освобождении hAutoResetEvent2, тоже переве дет его в занятое состояние. Оба потока просто зависнут, первый будет ждать осво бождения объекта, захваченного вторым потоком, а второй — освобождения объек та, захваченного первым. WaitForMultipleObjects гарантирует, что такого не случится никогда.

Тут возникает интересный вопрос. Если несколько потоков ждет один объект ядра, какой из них пробудится при освобождении этого объекта? Официально Microsoft отвечает на этот вопрос так: «Алгоритм действует честно" Что это за алгоритм, Micro soft не говорит, потому что нс хочст связывать себя обязательствами всегда придер живаться именно этого алгоритма. Она утверждает лишь одноесли объект ожидает ся несколькими потоками, то всякий раз, когда этот объект переходит в свободное состояние, каждый из них получает шанс на пробуждение.

Таким образом, приоритет потока не имеет значенияпоток с самым высоким приоритетом не обязательно первым захватит объект. Не получает преимущества и поток, который ждал дольше всех. Есть даже вероятность, что какой-то поток сумеет повторно захватить объект. Конечно, это было бы нечестно по отношению к другим потокам, и алгоритм пытается не допустить этого. Но никаких гарантий нет.

На самом деле этот алгоритм просто использует популярную схему "первым во шел — первым вышел" (FIFO). B принципе, объект захватывается потоком, ждавшим дольше всех. Но в системе могут произойти какие-то события, которые повлияют на окончательное решение, и ил-за этого алгоритм становится менее предсказуемым. Вот почему Microsoft и не хочет говорить, как именно он работает. Одно из таких собы тий — приостановка какого-либо потока. Если поток ждет объект и вдруг приоста навливается, система просто забывает, что он ждал этот объект. А причина в том, что нет смысла планировать приостановленный поток. Когда он в конце концов возоб новляется, система считает, что он только что начал ждать данный объект.

Учитывайте это при отладке, поскольку в точках прерывания (breakpoints) все потоки внутри отлаживаемого процесса приостанавливаются. Отладка делает алго ритм FIFO в высшей степени непредсказуемым из-за частых приостановки и возоб новления потоков процесса.

События

События - самая примитивная разновидность объектов ядра. Они содержат счетчик числа пользователей (как и все объекты ядра) и две булевы переменные: одна сооб щает тип данного объекта-события, другая — его состояние (свободен или занят).

События просто уведомляют об окончании какой-либо операции. Объекты-собы тия бывают двух типов: со сбросом вручную (manual-reset events) и с автосбросом (auto-reset events). Первые позволяют возобновлять выполнение сразу нескольких ждущих потоков, вторые — только одного.

Объекты-события обычно используют в том случае, когда какой-то поток выпол няет инициализацию, а затем сигнализирует другому потоку, что тот может продол жить работу. Инициализирующий поток переводит объект "событие» в занятое состо яние и приступает к своим операциям. Закончив, он сбрасывает событие в свободное состояние. Тогда другой поток, который ждал перехода события в свободное состоя ние, пробуждается и вновь становится планируемым.

Объект ядра «событие" создается функцией CreateEvent:

HANDLE CreateEvent(

PSECURITY_ATTRIBUTES psa, BOOL fManualReset, BOOL fInitialState, PCTSTR pszName);

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

Пареметр fManualReset (булева переменная) сообщает системе, хотите Вы создать событие со сбросом вручную (TRUE) или с автосбросом (FALSE). Параметру fInitialState определяет начальное состояние события — свободное (TRUE) или занятое (FALSE). После того как система создает объект событие, CreateEvent возвращает описатель события, специфичный для конкретного процесса. Потоки из других процессов мо гут получить доступ к этому объекту: 1) вызовом CreateEvent с тем же параметром pszName;, 2) наследованием описателя; 3) применением функции DuplicateHandle;, и 4) вызовом OpenEvent c передачей в параметре pszName имени, совпадающего с ука занным в аналогичном параметре функции CreateEvent. Вот что представляет собой функция

OpenEvent.

HANDLE OpenEvent( DWORD fdwAccess, BOOL fInhent, PCTSTR pszName);

Ненужный объект ядра "событие» следует, как всегда, закрыть вызовом CloseHandle Создав собьпис, Вы можете напрямую управлять его состоянием. Чтобы перевес ти его в свободное состояние, Вы вызываете:

BOOL SetEvenT(HANDLE hEvenеt);

А чтобы поменять его на занятое

BOOL ResetEvent(HANDLE hEvent);

Вот так все просто

Для событий с автосбросом действует следующее правило Когда его ожидание потоком успешно завершается, этот объект автоматически сбрасывается в занятое состояние. Отсюда и произошло название таких объектов-событий Для этого объек та обычно не требуется вызывать ResetEvent, поскольку система сама восстанавливает его состояние А для событий со сбросом вручную никаких побочных эффектов ус пешного ожидания не предусмотрено.

Рассмотрим небольшой пример тому, как на практике использовать объекты ядра «событие» для синхронизации потоков Начнем с такого кода.

// глобальный описатель события со сбросом вручную (в занятом состоянии)

HANDLE g_hEvent;

int WINAPI WinMain( )

{

//создаем объект "событие со сбросом вручную (в занятом состоянии)

g_hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);

//порождаем три новых потока

HANDLE hThread[3];

DWORD dwThreadTD;

hThread[0] = _beginthreadex(NULL, 0, WordCount, NULL, 0, &dwThreadlD);

hThread[1] = _beginthreadex(NULL, 0, SpellCheck, NULL, 0, &dwThreadID);

hTbread[2] = _beginthreadex(NULL, 0, GrarrmarCheck, NULL, 0, &dwThreadID);

OpenFileAndReadContentsIntoMemory( );

// разрешаем всем грем потокам обращаться к памяти

SetEvent(g__hEvent),

}

DWORD WINAPI WordCount(PVOID pvParam)

{

//ждем, когда в память будут загружены данные из файла

WaitForSingleObject(g_hEvent, iNeiNITE);

//обращаемся к блоку памяти

return(0);

}

DWORD WINAPI SpellCheck(PVOID pvParam)

{

//ждем, когда в память будут загружены данные из файла

WaitForSingleObject(g_hFvent, INFINITE);

//обращаемся к блоку пэмяти

return(0};

}

DWORD WINAPI GrammarCheck(PVOID pvParam)

{

//ждем, когда в память будут загружены данные из файла

WaitForSingleObject(g_hFvent, INFINITE);

//обращаемся к блоку памяти

return(0);

}

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

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

Загрузив нужные данные, первичный поток вызывает SetEvent, которая переводит событие в свободное состояние. В этот момент система пробуждает три вторичных потока, и они, вновь получив процессорное время, обращаются к блоку памяти За метьте, что они получают доступ к памяти в режиме только для чтения. Это единствен ная причина, по которой все три потока могут выполняться одновременно

Если событие со сбросом вручную заменить на событие с автосбросом, програм ма будет вести себя совершенно иначе После вызова первичным потоком функции SetEvent система возобновит выполнение только одного из вторичных потоков. Ка кого именно — сказать заранее нельзя. Остальные два потока продолжат ждать.

Поток, вновь ставший планируемым, получает монопольный доступ к блоку па мяги, где хранятся данные, считанные из файла Давайте перепишем функции пото ков так, чтобы перед самым возвратом управления они (подобно функции WinMain) вызывали SetFvent Теперь функции потоков выглядят следующим образом:

DWORD WINAPI WordCount(PVOID pvParam)

{

//ждем, когда в память будут загружены данные из файла

WaitForSingleObject(a_hEvent, INFINITE);

//обращаемся к блоку памяти

SetEvent(g_hEvent); return(0);

}

DWORD WINAPI SpellCneck(PVOID pvParam)

{

// ждем, когда в память будут загружены данные из файла