Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Конспект лекций 2009.doc
Скачиваний:
43
Добавлен:
13.11.2019
Размер:
2.3 Mб
Скачать

3.6.4Синхронизация потоков с помощью объектов ядра

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

Хотя механизмы синхронизации в пользовательском режиме обеспечивают высокое быстродействие, им свойствен ряд ограничений, и во многих приложениях они просто не будут работать. Например, Intertocked-функции оперируют только с отдельными переменными и никогда не переводят поток в состояние ожидания. Эту задачу можно решить с помощью критических секций, но они подходят лишь в тех случаях, когда требуется синхронизировать потоки в рамках одного процесса. Кроме того, при использовании критических секций легко попасть в ситуацию взаимной блокировки потоков, потому что задать предельное время ожидания входа в критическую секцию нельзя.

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

Ранее уже были упомятнуты некоторые объекты ядра, в том числе процессы, потоки и задания. Почти все они годятся и для решения задач синхронизации. В случае синхронизации потоков о каждом из этих объектов говорят, что он находится либо в свободном (signaled state), либо в занятом состоянии (nonsignaled state) Переход из одного состояния в другое осуществляется по правилам, определенным в операционной системе для каждого из объектов ядра Так, объекты ядра "процесс" сразу после создания всегда находятся в занятом состоянии. В момент завершения процесса операционная система автоматически освобождает его объект ядра "процесс", и он навсегда остается в этом состоянии.

Объект ядра "процесс" пребывает в занятом состоянии, пока выполняется сопоставленный с ним процесс, и переходит в свободное состояние, когда процесс завершается. Внутри этого объекта поддерживается булева переменная, которая при создании объекта инициализируется как FALSE ("занято"). По окончании работы процесса операционная система меняет значение этой переменной на TRUE, сообщая тем самым, что объект свободен.

В коде, проверяющем, выполняется ли процесс в данный момент, необходимо лишь вызвать функцию, которая просит операционную систему проверить значение булевой переменной, принадлежащей объекту ядра "процесс". Можно также сообщить системе, чтобы она перевела Ваш поток в состояние ожидания и автоматически пробудила его при изменении значения буле вой переменной с FALSE на TRUE. Тогда появляется возможность заставить поток в родительском процессе, ожидающий завершения дочернего процесса, просто заснуть до освобождения объекта ядра, идентифицирующего дочерний процесс. Аналогичные правила распространяются и на объект ядра «поток».

Ниже перечислены объекты ядра бывают в свободном или занятом состоянии:

  • процессы;

  • потоки;

  • задания;

  • файлы;

  • консольный ввод;

  • уведомления об изменении файлов;

  • события;

  • ожидаемые таймеры;

  • семафоры;

  • мьютексы;

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

Wait-функции позволяют потоку в любой момент приостановиться и ждать освобождения какого-либо объекта ядра. Из всего семейства этих функций чаще всего используется WaitForSingleObject:

DWORD WaitForSingleObject( HANDLE hObject, DWORD dwMilliseconds);

Когда поток вызывает эту функцию, первый параметр, hObject, идентифицирует объект ядра, поддерживающий состояния "свободен-занят" (То есть любой объект, упомянутый в списке выше) Второй параметр, dwMilliseconds, указывает, сколько времени (в миллисекундах) поток готов ждать освобождения объекта. Следующий вызов сообщает системе, что поток будет ждать до тех пор, пока не завершится процесс, идентифицируемый описателем hProcess.

WaitForSingleObject(hProcess, INFINITE);

В данном случае константа INFINITE, передаваемая во втором параметре, подсказывает системе, что вызывающий поток готов ждать этого события хоть целую веч ность. Именно эта коистанта обычно и передается функции WaitForSingleObject, но Вы можете указать любое значение в миллисекундах. Кстати, константа INFINITE опре делена как 0xFFFFFFFF (или -1). Разумеется, передача INFINlTE не всегда безопасна Если объект так и не перейдет в свободное состояние, вызывающий поток никогда не проснется; но тратить драгоценное процессорное время он при этом не будет

Вот пример, иллюстрирующий, как вызывать WaitForSingleObject co значением таймаута, отличным от INFINITE

DWORD dw = WaitForSlngleObject(hProcess, 5000);

switch (dw)

{

case WAIT_OBJECT_0:

// процесс завершается

break;

case WAIT_TIMEOUT:

// процесс не завершился в течение 5000 мс

break;

case WAIT_FAILED:

// неправильный вызов функции (неверный описатель?)

break;

}

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

Возвращаемое значение функции WaitForSingleObject указывает, почему вызывающий поток снова стал планируемым Если функция возвращает WAIT_OBJECT_0, объект свободен, а если WAIT_TIMEOUT — заданное время ожидания (таймаут) истекло. При передаче неверного параметра (например, недопустимого описателя) WaitForSing leObject возвращает WAIT_ FAILED. Чтобы выяснить конкретную причину ошибки, вы зовите функцию GetLastError.

Функция WaitForMultipleObjects аналогична WaitForSingleObject c тем исключением, что позволяет ждать освобождения сразу нескольких объектов или какого-то одного из списка объектов:

DWORD WaitForMultipleObjects( DWOHD dwCount, CONST HANDLE* phObjects, BOOL fWaitAll, DWORD dwMilliseconds);

Параметр dwCount определяет количество интересующих Вас объектов ядра Его значениедолжло быть в пределах от 1 до MAXIMUM_WAIT_OBJECTS (в заголовочных файлах Windows оно определено как 64). Параметр phObject — это указатель на массив описателей объектов ядра.

WaitForMultipleObjects приостанавливает поток и засгавляет его ждать освобождения либо всех заданных объектов ядра, либо одного из них. Параметр fWaitAll как раз и определяет, чего именно Вы хотите от функции. Если он равен TRUE, функция не даст потоку возобновить свою работу, пока не освободятся все объекты.

Параметр dwMilliseconds идентичен одноименному параметру функции WaitFor SingleObject Если указать конкретное время ожидания, то no его истечении функция в любом случае возвращает управление. И опять же, в этом параметре обычно передают INFINITE (будьте внимательны при написании кода, чтобы не создать ситуацию взаимной блокировки).

Возвращаемое значение функции WaitForMultipleObjects сообщает, почему возоб новилосъ выполнение вызвавшего ее потока Значения WAIT_FAILED и WAIT_TIMEOUT никаких пояснений не требуют. Если Вы передали TRUE в параметре fWaitAll и все объекты перешли в свободное состояние, функция возвращает значение WAIT_OBJECT_0. Если fWaitAll приравнен FALSE, она возвращает управление, как только ос вобождается любой из объектов. Для выяснения какой объект освободился, возвращается значение от WAIT_OBJECT_0 до WAIT_OBJECT_0 + dwCount - 1. Иначе говоря, если возвращаемое значение не равно WAIT_TIMEOUT или WAIT_FAILED, вычтитая из него значение WAlT_OBJECT_0, получаем индекс в массиве описателей, на который указывает второй параметр функции WaitForMultipleObjects. Этот индекс и определяет, какой объект перешел в незанятое состояние. Поясню сказанное на примере.

HANDLE h[3];

h[0] = hProcess1;

h[1] = hProcess2;

h[2] = hProcess3,

DWORD dw = WaitForMultipleObjects(3, h, FALSE, 5000);

switch (dw)

{

case WAIT_FAILED:

// неправильный вызов функции (неверный описатель?)

break;

case WAIT_TIMEOUT:

// ни один из объектов не освободился в течение 5000 мс

break;

case WAIT_OBJECT_0 + 0:

// завершился процесс, идентифицируемый h[0], т e описателем (hProcess1)

break;

case WATT_OBJECT_0 + 1:

// завершился процесс, идентифицируемый h[1], т e описателем (hProcess2)

break;

case WAIT_OBJECT_0 + 2:

// завершился процесс, идентифицируемый h[2], т. e описателем (hProcess3)

break;

}

Если Вы передаете FALSE в параметре fWaitAll, функция WaitForMultipleObjects сканирует массив описателей (начиная с нулевого элемента), и первый же освободив шийся объект прерывает ожидание Это может привести к нежелательным последствиям. Например, поток ждет завершения трех дочерних процессов; при этом функции передан массив с их описателями. Если завершается процесс, описатель которого находится в нулевом элементе массива, WaitForMultipleObjects возвращает управление. Теперь поток может сделать то, что ему нужно, и вновь вызвать эту функцию, ожидая завершения другого процесса. Если поток передаст те же три описателя, функция немедленно всрнст управление, и вновь вернётся значение WAIT_OBJECT_0. Таким образом, до удаления описателей тех объектов, об освобождении которых функция уже сообщила, код будет работать некорректно. Важная особенность функции WaitForMultipleObjects - она выполняет все действия на уровне атомарного доступа.

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

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

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

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

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

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

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

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

HANDLE CreateEvent( PSECURITY_ATTRIBUTES psa, BOOL fManualReset, BOOL fInitialState, PCTSTR pszName);

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

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

  • вызовом CreateEvent с тем же параметром pszName;

  • наследованием описателя;

  • применением функции DuplicateHandle;

  • вызовом 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] = CreateThread(NULL, 0, WordCount, NULL, 0, &dwThreadlD);

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

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

OpenFileAndReadContentsIntoMemory( );

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

SetEvent(g__hEvent),

}

DWORD WINAPI WordCount(PVOID pvParam)

{

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

WaitForSingleObject(g_hEvent, INFINITE);

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

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) вызывали SetEvent Теперь функции потоков выглядят следующим образом:

DWORD WINAPI WordCount(PVOID pvParam)

{

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

WaitForSingleObject(a_hEvent, INFINITE);

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

SetEvent(g_hEvent);

return(0);

}

DWORD WINAPI SpellCneck(PVOID pvParam)

{

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

WaitForSingleObject(g htvent, INFINITE);

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

SetEvent(g_hEvent);

return(0);

}

DWORD WINAPI GrammarCheck(PVOID pvParam)

{

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

WaitForSingleObject(g_hEvent, INFINITF);

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

SetEvent(g_hEvent);

return(0);

}

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

Ещё одна функция для работы с объектами-событиями:

BOOL PulseEvent(HANDLE hEvent);

PuteeEvent освобождает событие и тут же переводит его обратно в занятое состояние; ее вызов равнозначен последовательному вызову SetEvent и ResetEvent. Если Вы вызываете PulseEvent для события со сбросом вручную, любые потоки, ждущие этот объект; становятся планируемыми. При вызове этой функции применительно к со бытию с автосбросом пробуждается только одип из ждущих потоков. А если ни один из потоков не ждет объект-событие, вызов функции не дает никакого эффекта

Ожидаемые таймеры (waitable timers) ~ это объекты ядра, которые самостоятельно переходят в свободное состояние в определенное время или через регулярные промежутки времени. Чтобы создать ожидаемый таймер, достаточно вызвать функцию CreateWaitableTimer.

HANDLE CreateWaitableTimer( PSECURITY_ATTRIBUTES psa, BOOL fManualReset, PCTSTR pszName);

Любой процесс может получить свой ("процессо-зависимый") описатель существующего объекта "ожидаемый таймер", вызвав OpenWaitableTimer.

HANDLE OpenWaitableTirrer( DWORD dwDesiredAccess, BOOL bInheritHandle, PCTSTR pszName);

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

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

BOOL SetWaitableTimer( HANDLE hTimer, const LARGE_INTEGER *pDueTime, LONG lPeriod, PTIMERAPCROUTINE pfnCompletionRoutine, PVOID pvArgToCotnpletionRoutine, BOOI fResume);

Эта функция принимает несколько параметров, в которых легко запутаться. Очевидно, что hTimer определяет нужный таймер. Следующие два параметра (pDиеТimе и lPeriod) используются совместно, первый из них задает, когда таймер должен сработать в первый раз, второй определяет, насколько часто это должно происходить в дальнейшем. Попробуем для примера установить таймер так, чтобы в первый раз он сработал 1 января 2002 года в 1:00 PM, а потом срабатывал каждые 6 часов.

// объявляем свои локальные переменные

HANDLE hTimer;

SYSTEMTIME st;

FILETIME ftLocal, ftUTC;

LARGE_INTEGER liUTC;

// создаем таймер с автосбросом

hTimer = CreateWaitableTimer(NULL, FALSE, NULL);

// таймер должен сработать в первый раз 1 января 2002 года в 1:00 PM

// но местному времени

st.wYear = 2002; // год

st.wMonth = 1; // январь

st.wOayOfWeek = 0; // игнорируется

st.wDay = 1, // первое число месяца

st.wHour = 13; // 1 PM

st.wMinute = 0; // 0 минут

st.wSecond = 0, // 0 секунд

st.wMilliseconds = 0; // 0 миллисекунд

SystemTimeToFileTime(&st, &ftLocal);

// преобразуем местное время в UTC-время

LocalFileTimeToFilelime(&ttLocal, &ftUTC);

// преобразуем FILETIME в LARGE_INTEGER из-за различий в выравнивании данных

liUTC.LowPart = ftUTC dwLowDateTime;

liUTC.HighPart = ftUTC dwHighDateTime;

// устанавливаем таймер

SetWaitablcTimer(hTimer, &liUTC, 6 * 60 * 60 * 1000, NULL, NULL, FALSE);

...

Этот фрагмент кода сначала инициализирует структуру SYSTEMTIME, определяя время первого срабатывания таймера (его перехода в свободное состояние). Я уста новил это время как местное. Второй параметр представляется как const LARGE_IN TEGER * и поэтому не позволяет напрямую использовать структуру SYSTEMTIME. Од нако двоичные форматы структур FILETIME и LARGE_INTEGER идентичны: обе содер жат по два 32-битных значения. Таким образом, мы можем преобразовать структуру SYSTEMTIME в FILETIME. Другая проблема заключается в том, что функция SetWaitable Timer ждет передачи времени в формате UTC (Coordinated Universal Time). Нужное преобразование легко осуществляется вызовом LocalFileTimeToFileTime

Поскольку двоичные форматы структур FILETIME, и LARGE_INTEGER идентичны, у Вас может появиться искушение передать в SetWaitableTimer адрес структуры FILETIME напрямую;

// устанавливаем таймер

SetWaitableTimer(hTirner, (PLARGE^INTEGER) &ftUTC, 6 * 60 * 60 * 1000, NULL, NULL, FALSE);

Однако это большая ошибка. Хотя двоичные форматы структур FILETIME и LARGE_INTEGER совпадают, выравнивание этих структур осуществляется по-разному. Адрес любой структуры FILETIME должен начинаться на 32-битной границе, а адрес любой структуры LARGE_INTEGER — на 64-битной. Вызов SetWaitableTimer с передачей ей структуры FILETIME может cpaботать корректно, но может и не сработать — все зависит от того, попадет ли начало структуры FlLETIME на 64-битную границу. В то же время компилятор гарантирует, что структура LARGE_INTEGER всегда будет начинаться на 64-битной границе, и по этому правильнее скопировать элементы FILETIME в элементы LARGE_INTEGER, а за тем передать в SetWaitableTtmer адрес именно структуры LARGE_INTEGER.

Процессоры x86 всегда "молча" обрабатываю ссылки на невыровненные данные. Поэтому передача в SetWaitableTimer адреса структуры FILETIME будет срабатывать, если приложение выполняется на машине с процессором x86. Однако другие процессоры (например, Alpha) в таких случаях, как правило, генерируют исключение EXCEPTION_DATATYPE_MISALIGNMENT, которое приводит к завершению процесса. Ошибки, связанные с выравниванием данных, — самый серьезный источник проблем при переносе на другие процессорные платформы программного кода, корректно работавшего на процессорах x86. Так что, обратив внимание на проблемы выравнивания данных сейчас, Вы сэкономите себе месяцы труда при переносе программы на другие платформы в будущем!

Чтобы разобраться в том, как заставить таймер срабатывать каждые 6 часов (на чиная с 1:00 PM 1 января 2002 года), рассмотрим параметр lPeriod функции SetWaitable Timer. Этот параметр определяет последующую частоту срабатывания таймера (в мс). Чтобы установить 6 часов, я передаю значение, равное 21 600 000 мс (т e. 6 часов * 60 минут • 60 секунд • 1000 миллисекунд).

Обычно нужно, чтобы таймер сработал только раз — через определенное (абсолютное или относительное) время перешел в свободное состояние и уже больше никогда не срабатывал Для этого достаточно передать 0 в параметре lPeriod. Затем можно либо вызвать CloseHandle, чтобы закрыть таймер, либо перенастроить таймер повторным вызовом SetWattableTimer с другими параметрами.

И о последнем параметре функции SetWaitableTimer — lResume. Он полезен на компьютерах с поддержкой режима сна («спящий режим», hibernate). Обычно в нем передают FALSE. Но если Вы, скажем, пишете программу-планировщик, которая позволяет устанавливать таймеры для напоминания о запланированных встречах, то должны передавать в этом параметре TRUE Когда таймер сработает, машина выйдет из режима сна (если она находилась в нем), и пробудятся потоки, ожидавшие этот таймер. Далее программа сможет проиграть какой-нибудь WAV-файл и вывести окно с напоминанием о предстоящей встрече. Если же Вы пере дадите FALSE в параметре fResume, объект-таймер перейдет в свободное состояние, но ожидавшие его потоки не получат процессорное время, пока компьютер не выйдет из режима сна

Ещё одна функция, связанная с ожидаемыми таймерами – это CancelWaitable Timer.

BOOL CancelWaitableTimer(HANDLE hTimer);

Эта очень простая функция принимает описатель таймера и отменяет его, после чего тот уже никогда не сработает, — если только не переустановить его повторным вызовом SetWaitableTimer. Если необходимо перенастроить таймер, то вызывать CancelWattableTimer перед повторным обращением к SetWaitableTimer не требуется; каждый вызов SetWaitableTimer автоматически отменяет предыдущие настройки перед установкой новых

В ОС есть возможность создать очередь асинхронных вызовов процедур (asynchronous procedure call, APC) для потока, вызывающего SetWaitableTimer в момент, когда таймер свободен. Обычно при обращении к функции SetWaitableTimer Вы передаете NULL в параметрах pfnCompletionRoutine и pvArgToCompletionRoutine. В этом случае объект-таймер переходит в свободное состояние в заданное время. Чтобы таймер в этот момент поместил в очередь вызов АРС-функции, нужно реализовать данную функцию и передать ее адрес в SetWaitableTimer. АРС-функция должна выглядеть примерно так:

VOID APIENTRY TimerAPCRoutine(PVOID pvArgToCompleUonRoutine, DWORD dwTimerLowValue, DWORD dwTimerHighValue)

{

// здесь делаем то, что нужно

}

Имя функции APC может быть любым. Она вызывается из того потока, который обратился к SetWaitableTimer в момент срабатывания таймера, — но только если вызывающий поток находится в "тревожном" (alertable) состоянии, т. e. ожидает этого в вызове одной из функций: SleepEx, WaitForSingleObjectEx, WaitForMultipleObjectsEx, MsgWaitForMultipleObjectsEx или SignalObjectAndWait. Если же поток этого не ожидает в любой из перечисленных функций, система не поставит в очередь АРС-функцию таймера. Тем самым система не даст АРС-очереди потока переполниться уведомлениями от таймера, которые могли бы впустую израсходовать колоссальный объем памяти.

Если в момент срабатывания таймера поток выполняет одну из перечисленных ранее функций, система заставляет его вызвать процедуру обратного вызова Первый ее параметр совпадает с параметром pvArgToCompletionRoutine, передаваемым в функцию SetWaitableTimer, Это позволяет передавать в TimerAPCRoutine какие-либо данные (обычно указатель на определенную Вами структуру) Остальные два парамет ра, dwTimerLowValue и dwTimerHighValue, задают время срабатывания таймера. Код, приведенный ниже, демонстрирует, как принять эту информацию и показать ее поль зователю.

VOID APIENTRY TimerAPCRoutine(PVOID pvArgToCompletionRoutine, DWORD dwTimerLowValue, DWORD dwT:merHighValue)

{

FILETIME ftUTC, ftLocal;

SYSTEMTIME st;

TCHAR szBuf[256];

// записываем время в структуру

FILETIME ftUTC.dwlowDateTime = dwTimerLowValue;

ftUTC.dwHighDateFime = dwTimerHighValue;

// преобразуем UTC-время в местное

FileTimeToLocalFileTime(&ftUTC, &ftLocal);

// преобразуем структуру FILETIME в структуру SYSTEMTIME,

// как того требуют функции GetDateFormat и GetTimeFormat

FileTimetoSystemTime(&ftLocal, &st);

// формируем строку с датой и временем, в которой

// сработал таймер

GetDateFormat(LOCALE_USER_DEFAULT, DATE_LONGDATE, &st, NULL, szBuf, sizeof(szBuf) / sizeof(TCHAR));

_tcscat(szBuf, __TEXT(' '));

GetTimeFormat(LOCALE_USER_DEFAULT, 0, &st, NULL, _tcschr(szBuf, 0), si/eof(szBuf) / sizeor(TCHAR) - _tcslen(sz8uf));

// показываем время пользователю

MessageBox(NULL, szBuf, "Timer went off at ... ", MB_OK); }

Функция "тревожного ожидания" возвращает управление только после обработки всех элементов АРС-очереди. Поэтому необходимо обеспечить, чтобы функция TimerAPCRoutine заканчивала свою работу до того, как таймер вновь подаeт сигнал (перейдет в свободное состояние). Иначе говоря, элементы не должны ставиться в АРС-очередь быстрее, чем они могут быть обработаны.

Следующий фрагмент кода показывает, как правильно пользоваться таймерами и APC:

void SomeFunc() {

//создаем таймер (его тип не имеет значения)

HANDLE hTimer = CreateWaitableTimer(NULL, TRUE, NULL);

// настраиваем таймер на срабатывание через 5 секунд

LARGE_INTEGER li = { 0 };

SetWaitableTimer(hTimer, &li, 5000, TimerAPCRoutine, NULL, FALSE);

// ждем срабатывания таймура в "тревожном" состоянии

SleepEx(INFINITE, TRUE);

CloseHandle(hTimer);

}

И последнее. Рассмотрим фрагмент кода:

HANDLE hTimer - CreateWaitableTimer(NULL, FALSE, NULL);

SetWaitableTimer(hTimer, ..., TimerAPCRoutine, );

WaitForSingleObjectEx(hTimer, INFINITE, TRUE);

Никогда ие пишите такой код, потому что вызов WaitForSingleObjectEx на деле заставляет дважды ожидать таймер — по описателю hTimer и в "тревожном" состоянии Когда таймер перейдет в свободное состояние, поток пробудится, что выведет eго из "тревожного" состояния, и вызова АРС-функции не последует. Правда, АРС-функции редко используются совместно с ожидаемыми таймерами, так как всегда можно дож даться перехода таймера в свободное состояние, а затем сделать то, что нужно.

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

Главное отличие ожидаемых таймеров от пользовательских таймеров (создаваемых функцией SetTimer)в том, что ожидаемые таймеры реализованы в ядре, а значит, не столь тяжеловесны, как таймеры User. Кроме того, это означает, что ожидаемые таймеры — объекты защищенные.

Таймеры User генерируют сообщения WM_TIMER, посылаемые тому потоку, который вызвал SetTimer (в случае таймеров с обратной связью) или создал определенное окно (в случае оконных таймеров). Таким образом, о срабатывании таймера User уведомляется только один поток А ожидаемый таймер позволяет ждять любому числу потоков, и, если это таймер со сбросом вручную, при его освобождении может пробуждаться сразу несколько потоков.

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

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

Для семафоров определены следующие правила:

  • когда счетчик текущего числа ресурсов становится больше 0, семафор переходит в свободное состояние,

  • если этот счетчик равен 0, семафор занят,

  • система не допускает присвоения отрицательных значений счетчику текущего числа ресурсов;

  • счетчик текущего числа ресурсов не может быть больше максимального числа ресурсов

  • Не путайте счетчик текущего числа ресурсов со счетчиком числа пользователей объекта-семафора

Объект ядра "семафор" создается вызовом CreateSemapbore

HANDLE CreateSemaphore( PSECURITY_ATTRIBUTE psa, LONG lInitialCount, LONG lMaximumCount, PCTRTR pszName)

Любой процесс может получить свой ("процессо-зависимый") описатель существующего объекта "семафор", вызвав OpenSemaphore

HANDLE OpenSemaphore( DWORD fdwAccess, BOOL bInhentHandle, PCTSTR pszName);

Параметр lMaximumCount сообщает системе максимальное число ресурсов, обрабатываемое Вашим приложением Поскольку это 32-битное значение со знаком, пре дельное число ресурсов можетдостигать 2 147 483 647 Параметр lInitiа1Соunt указы вает, сколько из этих ресурсов доступно изначально (на данный момент)

Рассмотрим вызов

HANDLE hSem = CreateSemaphore(NULL, 0, 5, NULL);

Это приводит к созданию семафора со счетчиком максимального числа ресурсов равным 5, при этом изначально ни один ресурс не доступен (Кстати, счетчик числа пользователей данного объекта ядра равен 1, так как объект только что создан, не запутайтесь в счетчиках) Поскольку счетчику текущего числа ресурсов присвоен 0 семафор находится в занятом состоянии А это значит, что любой поток, ждущий се мафор, просто засыпает

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

Если Wait-функция определяет, что счетчик текущего числа ресурсов равен 0 (семафор занят), система переводит вызывающий поток в состояние ожидания Когда другой поток увеличит значение этого счетчика, система вспомнит о ждущем потоке и снова начнет выделять ему процессорное время (а он, захватив ресурс, уменьшит значение счетчика на 1).

Поток увеличивает значение счетчика текущего числа ресурсов, вызывая функцию ReleaseSemaphore

BOOL ReleaseSemaphore( HANDLE hSem, LONG lReleaseCount, PLONG p]PreviousCount);

Она просто складывает величину lReleaseCount со значением счетчика текущего числа ресурсов. Обычно в параметре lReleaseCount передают 1, однако возможно передать и большие значения . Функция возвращает исходное значение счетчика ресурсов в *plPreviousCount Если Вас не интересует это значение (а в большинстве программ так оно и есть), передайте в параметре plPreviousCount значение NULL.

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

Объекты ядра "мьютексы" гарантируют потокам взаимоисключающий доступ к единственному ресурсу. Отсюда и произошло название этих объектов (mutual exclusion, mutex). Они содержат счетчик числа пользователей, счетчик рекурсии и переменную, в которой запоминается идентификатор потока. Мьютексы ведут себя точно так же, как и критические секции. Однако, если последние являются объектами пользовательского режима, то мьютексы — объектами ядра. Кроме того, единственный объект-мьютекс позволяет синхронизировать доступ к ресурсу нескольких потоков из разных процессов; при этом можно задать максимальное время ожидания доступа к ресурсу.

Идентификатор потока определяет, какой поток захватил мьютекс, а счетчик рекурсий — сколько раз. У мьютексов много применений, и это наиболее часто используемые объекты ядра. Как правило, с их помощью защищают блок памяти, к которому обращается множество потоков Если бы потоки одновременно использовали ка кой-то блок памяти, данные в нем были бы повреждены. Мьютексы гарантируют, что любой поток получает монопольный доступ к блоку памяти, и тем самым обеспечивают целостность данных.

Для мьютексов определены следующие правила:

  • если его идентификатор потока равен 0 (у самого потока не может быть та кой идентификатор), мьютекс не захвачен ни одним из потоков и находится в свободном состоянии;

  • если его идентификатор потока не равен 0, мьютекс захвачен одним из потоков и находится в занятом состоянии;

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

Для использования объекта-мьютекса один из процессов должен сначала создать его вызовом CreateMutex:

HANDLE CreateMutex( PSECURITY_ATTRIBUTES psa, BOOL fIniLialOwner, PCTSTR pszName);

Любой процесс может получить свой ("процессо-зависимый") описатель существующего объекта "мьютекс", вызвав OpenMutex:

HANDLE OpenMutex( DWORD fdwAccess, 800L bInheritHandle, PCTSTR pszName);

Параметр fInitialOwner опрсдсляст начальное состояние мьютекса. Если в нем передается FALSE (что обычно и бывает), объект-мьютекс не принадлежит ни одному из потоков и поэтому находится в свободном состоянии. При этом его идентификатор потока и счетчик рекурсии равны 0. Если же в нем передается TRUE, идентификатор потока, принадлежащий мьютексу, приравнивается идентификатору вызывающего потока, а счетчик рекурсии получает значение 1. Поскольку теперь идентификатор потока отличен от 0, мьютекс изначально находится в занятом состоянии.

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

Если Wait-функция определяет, что у мьютекса идентификатор потока не равен 0 (мьютекс занят), вызывающий поток переходит в состояние ожидания. Система запоминает это и, когда идентификатор обнуляется, записывает в него идентификатор ждущего потока, а счетчику рекурсии присваивает значение 1, после чего ждущий поток вновь становится планируемым. Все проверки и изменения состояния объекта-мьютекса выполняются на уровне атомарного доступа.

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

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

BOOL ReleaseMutex(HANDLE hMutex);

Эта функция уменьшает счстчик рекурсии в объекте-мьютексе на 1. Если данный объект передавался во владение потоку неоднократно, поток обязан вызвать Release Mutex столько раз, сколько необходимо для обнуления счстчика рекурсии. Как только счетчик станет равен 0, переменная, хранящая идентификатор потока, тоже обнулится, и объект-мьютекс освободится. После этого система проверит, ожидают ли освобождения мьютекса какие-нибудь другие потоки. Если да, система "по-честному" выберет один из ждущих потоков и передаст ему во владение объект-мьютекс.

Объект-мьютекс отличается от остальных объектов ядра тем, что занявшему его по току передаются права на владение им. Прочие объекты могут быть либо свободны, либо заняты — вот, собственно, и все. А объекты-мьютексы способны еще и запоминать, какому потоку они принадлежат. Если какой-то посторонний поток попытается освободить мьютекс вызовом функции ReleaseMutex, то она, проверив идентифика торы потоков и обнаружив их несовпадение, ничего делать не станет, а просто вернет FALSE. Тут же вызвав GetLastError, Вы получите значение ERROR_NOT_OWNER.

Отсюда возникает вопрос а что будет, если поток, которому принадлежит мью текс, завершится, не успев его освободить? В таком случае система считает, что произошел отказ от мьютекса, и автоматически переводит его в свободное состояние (сбрасывая при этом все его счетчики в исходное состояние). Если этот мьютекс ждут другие потоки, система, как обычно, "по-честному" выбирает один из потоков и по зволяет ему захватить мьютекс. Тогда Wait-функция возвращает потоку WAIT_ABANDONED вместо WAIT_OBJECT_0, и тот узнает, что мьютскс освобожден некорректно. Данная ситуация, конечно, не самая лучшая. Выяснить, что сделал с защищенными дан ными завершенный поток — бывший владелец объекта-мьютекса, увы. невозможно.

В реальности программы никогда специально не проверяют возвращаемое значение на WAIT_ABANDONED, потому что такое завершение потоков происходит очень редко, и как правило, при вызове функции ExitThread или TerminateThread

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

Таблица 3.1. Сравнение мьютексов и критических секций

Характеристики

Объект-мьютекс

Обьект — критическая секция

Быстродействие

Малое

Высокое

Возможность использования за границами процесса

Да

Нет

Объявление

HANDLE hmfx;

CRITICAL_SECTION cs;

Инициализация

hmtx = CreateMutex (NULL, FALSE, NULL);

InitializeCriticalSection(&cs);

Очистка

CloseHandle(hmtx);

DeleteCriticalSection(&cs);

Бесконечное ожидание

WaitForSingleObject (hmtx, INFINITE);

EnterCrittcalSection(&cs);

Ожидание в течение 0 мс

WaitForSingleObject (hmtx, 0);

TryEnterCriticalSection (&cs);

Ожидание в течение произвольного периода времени

WaitForSingleObject (hmtx, dwMilliseconds);

Невозможно

Освобождение

ReleaseMutex(hmtx);

LeaveCriticalSecliun(&cs);

Возможность параллельного ожидания других объектов ядра

Да (с помощью WaitForMultipleObjects или аналогичной функции)

Нет