Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Рихтер Дж., Назар К. - Windows via C C++. Программирование на языке Visual C++ - 2009

.pdf
Скачиваний:
6250
Добавлен:
13.08.2013
Размер:
31.38 Mб
Скачать

296 Часть II. Приступаем к работе

(g_hMainDlg == NULL) &&

(_tcscmp(g_szSharedRequestAndResultBuffer, g_ erShutdown) == 0); if{!fShutdown) {

//,обрабатывавм клиентский запрое-(инвертиру^стрску) _tcsrev(g_szSharedRequestAndResultBuffer);

}

// разрешаем клиенту обработать результат запроса:

SetEvent(g_hevtResultReturned);

}

// клиент хочет завершить программу — выходим return(0);

}

///////////////////////////////////////////////////////////////////////////////

///

BOOL Dlg_OnInitDialog (HWND hwnd, HNND hwndFocus,LPARAM lParam){

chSETDLGICONS(hwnd, IDI_HANDSHAKE);

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

Edit_SetText(GetDlgItem(hwnd, IDC_REQUEST), TEXT{"Some test data"));

// Сохраняем описатель главного окна g_hMainDlg = hwnd;

return(TRUE»

}

///////////////////////////////////////////////////////////////////////////////

///

void Dlg_OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify)

{

Switch (id) {

case IDCANCEL: EndDialog(hwnd, id); break;

сазе IDC_SUBMIT: // передаем запрос серверному потоку

Глава 9. Синхронизация потоков с использованием объектов ядра.docx 297

//копируем строку запроса в разделяемый блок памяти

Edit_GetText(GetDlgItem(hwnd, IDC_REQUEST), g_szSharedRequestAndResultBuffer, _countof(g_szSharedRequestAndResultBuffer));

//даем знать серверному потоку, что в буфере появился запрос

SetEvent(g_hevtRequestSubmitted);

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

WaitForSingleObject(g_hevtResultReturned, INFINITE);

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

Edit_SetText(GetDlgItem(hwnd, IDC_RESULT),

g_szSharedRequestAndResultBuffer);

break;

}

}

///////////////////////////////////////////////////////////////////////////////

///

INT_PTR WINAPI Dlg_Proc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {

switch (uMsg) {

chHANDLE_DLGMSG(hwnd, WM_INITDIALOG,

Dlg_OnInitDialog);

chHANDLE_DLGMSG(hwnd, WM_COMMAND,

Dlg_OnCommand);

}

 

 

 

return(FALSE);

 

}

///////////////////////////////////////////////////////////////////////////////

///

int WINAPI _tWinMain(HINSTANCE hInstanceExe, HINSTANCE, PTSTR, int) {

//создаем и инициализируем два события с автосбросом в занятом

//состоянии

g_hevtRequestSubmitted =

CreateEvent(NULL,

FALSE,

FALSE,

NULL);

g.hevtResultReturned =

CreateEvent(NULL,

FALSE,

FALSE,

NULL);

 

 

 

 

 

// порождаем серверный поток

DWORD dwThreadID;

HANDLE hThreadServer = chBEGINTHREADEX(NULL, 0, ServerThread, NULL, 0, &dwThreadID);

298Часть II. Приступаем к работе

//отображаем пользовательский интерфейс клиентского потока

DialogBox(hInstanceExe, MAKEINTRESOURCE(IDD_HANDSHAKE), NULL, Dlg_, Proc); g_hMainDlg = NULL;

//пользовательский интерфейс клиента закрывается - надо завершить

//серверный поток

_tcscpy_s(g_szSharedRequestAndResultBuffer

_countof(g_szSha redRequestAndResultBuffer), g_szServerShutdown); SetEvent(g_hevtRequestSubmitted);

// ждем от серверного потока подтверждения и его завершения

HANDLE h[2];

h[0] = g_hevtResultReturned; h[1] = hThreadServer;

WaitForMultipleObjects(2, h, TRUE, INFINITE);

//проводим должную очистку

CloseHandle(hThreadServer); CloseHandle(g_hevtRequestSubmitted); CloseHandle(g_hevtResultReturned);

// клиентский поток завершается вместе со всем процессом return(0);

}

///////////////////////////////// End of File /////////////////////////////////

Ожидаемые таймеры

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

HANDLE CreateWaitableTimer(

PSECURITY_ATTRIВUTES psa,

BOOL bManualReset,

PCTSTR pszName);

О параметрах psa и pszName я уже рассказывал в главе 3. Разумеется, любой процесс может получить свой («процессозависимый») описатель существующего объекта «ожидаемый таймер», вызвав OpenWaitableTimer.

HANDLE OpenWaitableTimer(

DWORD dwDesiredAccess,

BOOL bInheritHandle,

PCTSTR pszName);

Глава 9. Синхронизация потоков с использованием объектов ядра.docx 299

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

Объекты «ожидаемый таймер» всегда создаются в занятом состоянии. Чтобы сообщить таймеру, в какой момент он должен перейти в свободное состояние, вы-

зовите функцию SetWaitableTimer.

BOOL SetWaitableTimer(

HANDLE hTimer,

const LARGE_INTEGER *pDueTime, LONG lPeriod,

PTIMERAPCROUTINE pfnCompletionRoutine, PVOID pvArgToCompletionRoutine,

BOOL bResume);

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

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

HANDLE hTimer; SYSTEMTIME st;

FILETIME ftLocal, ftUTC; LARGE.INTEGER liUTC;

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

hTimer = CreateWaitableTimer(NULL, FALSE, NULL);

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

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

st.wYear

= 2008;

// год

st.wMonth

= 1;

// январь

st.wDayOfWeek

= 0;

// игнорируется

st.wDay

= 1;

// первое число месяца

st.wHour

= 13;

// 1

РИ

st.wMinute

= 0;

// 0

минут

8t.wSecond

= 0;

// 0

секунд

st.wMilliseconds

= 0;

// 0

миллисекунд

SystemTimeToFileTime(&st, &ftLocal);

300 Часть II. Приступаем к работе

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

LocalFileTimeToFileTime(&ftLocal, &ftUTC);

//преобразуем FILETIME в LARGE_INTEGER из-за различий в выравнивании данных liUTC.LowPart = ftUTC.dwLowDateTime;

liUTC.HighPart = ftUTC.dwHighDateTime;

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

SetWaitableTimer(hTimer, &liUTC, 6 * 60 * 60 * 1000,

NULL, NULL, FALSE); …

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

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

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

SetWaitableTimer(hTimer, (PLARGE_INTEGER) &ftUTC, 6 * 60 * 60 * 1000, NULL, NULL, FALSE);

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

Примечание. Процессоры x86 всегда «молча» обрабатывают ссылки на невыровненные данные. Поэтому передача в SetWaitableTimer адреса структуры FILETIME будет срабатывать, если приложение выполняется на

Глава 9. Синхронизация потоков с использованием объектов ядра.docx 301

машине с процессором x86. Однако другие процессоры (например, Alpha) в таких случаях, как правило, генерируют исключение

EXCEPTION_DATATYPE_MISALIGNMENT, которое приводит к заверше-

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

Чтобы разобраться в том, как заставить таймер срабатывать каждые 6 часов (начиная с 1Ю0 PM 1 января 2008 года), рассмотрим параметр lPeriod функции SetWaitableTimer. Этот параметр определяет последующую частоту срабатывания таймера (в мс). Чтобы установить 6 часов, я передаю значение, равное 21600 000 мс (т. е. 6 часов • 60 минут • 60 секунд • 1000 миллисекунд). Между прочим, вызов SetWatiMeTimer сработает и при передаче абсолютного времени и даты в прошлом, например 1:00 PM 1 января 1975 г.

Вместо того чтобы устанавливать время первого срабатывания таймера в абсолютных единицах, вы можете задать его в относительных единицах (в интервалах по 100 нс), просто передав отрицательное значение в параметре pDueTime. Поскольку мы обычно не пользуемся 100-нс интервалами, возможно вам пригодятся следующие соотношения: 1 с = 1000 мс = 1000 000 мс =10 000 000 интервалов по 100 нс.

Следующий код демонстрирует, как установить таймер на первое срабатывание через 5 секунд после вызова SetWaitableTimer.

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

HANDLE hTimer; LARGE_INTEGER li;

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

hTimer * CreateWaitableTimer(NULL, FALSE. NULL);

//Таймер должен сработать через 5 секунд после вызова SetWaitableTimer.

//задаем время в интервалах по 100 нс.

const int nTimerUnitsPerSecond = 10000000;

//Делаем полученное значение отрицательным, чтобы SetWaitableTimer

//знала: нам нужно относительное, а не абсолютное время. li.QuadPart =-(5 * nTimerUnitsPerSecond);

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

SetWaitableTimer(hTimer. &li, 6 * 60 * 60 = 1000,

NULL, NULL, FALSE); …

Обычно нужно, чтобы таймер сработал только раз — через определенное (абсолютное или относительное) время перешел в свободное состояние и

302 Часть II. Приступаем к работе

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

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

Рассмотрение ожидаемых таймеров было бы неполным, пропусти мы функ-

цию CancelWaitableTimer.

BOOL CancelWaitableTimer(HANDLE hTimer);

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

Ожидаемые таймеры и АРС-очередь

Теперь вы знаете, как создавать и настраивать таймер. Вы также научились приостанавливать потоки на таймере, передавая его описатель в WaitForSingleObjects или WaitForMultipleObjects. Однако у вас есть возможность создать очередь асинхронных вызовов процедур (asynchronous procedure call, APC) для потока, вызывающего SetWaitableTimer в момент, когда таймер свободен.

Обычно при обращении к функции SetWaitableTimer вы передаете NULL в па-

раметрах pfnCompletionRoutine и pvArgToCompletionRoutine. В этом случае объ-

ект-таймер переходит в свободное состояние в заданное время. Чтобы таймер в этот момент поместил в очередь вызов АРС-функции, нужно реализовать данную функцию и передать ее адрес в SetWaitableTimer. АРС-функция должна выглядеть примерно так:

Глава 9. Синхронизация потоков с использованием объектов ядра.docx 303

VOID APIENTRY TimerAPCRoutine(PVOID pvArgToCompletionRoutine,

DWORD dwTimerLowValue, DWORD dwTimerHighValue) {

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

}

Я назвал эту функцию TimerAPCRoutine, но вы можете назвать ее как угодно. Она вызывается из того потока, который обратился к SetWaitableTimer в момент срабатывания таймера, — но только если вызывающий поток находится в «тревожном» (alertable) состоянии, т. е. ожидает этого в вызове одной из функций:

SleepEx, WaitForSingleObjectEx, WaitForMultipleObjectsEx, MsgWaitForMultipleObjectsEx или SignalObjectAndWait. Если же поток этого не ожидает в любой из перечисленных функций, система не поставит в очередь АРС-функцию таймера. Тем самым система не даст АРС-очереди потока переполниться уведомлениями от таймера, которые могли бы впустую израсходовать колоссальный объем памяти. Подробнее о тревожном ожидании см. в главе 10.

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

ру). Остальные два параметра, dwTimerLowValue и dwTimerHighValue, задают время срабатывания таймера. Код, приведенный ниже, демонстрирует, как принять эту информацию и показать ее пользователю.

VOID APIENTRY TimerAPCRoutine(PVOID pvArgToCompletionRoutine,

DWORD dwTimerLowValue, DWORD dwTimerHighValue) {

FILETIME ftUTC, ftLocal;

SYSTEMTIME st;

TCHAR szBuf[256];

//записываем время в структуру FILETIME ftUTC.dwLowDateTime = dwTimerLowValue; ftUTC.dwHighDateTime = dwTimerHighValue;

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

FileTimeToLocalFileTime(&ftUTC. &ftLocal);

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

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

FileTimeToSystemTime(&ftLocal, &st);

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

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

304 Часть II. Приступаем к работе

GetDateFormat(LOCALE_USER_DEFAULT, DATE_LONGDATE,

&st, NULL, szBuf, _countof(szBuf));

_tcscat_s(szBuf, _countof(szBuf), TEXT(" "));

GetTimeFormat(LOCALE_USER_DEFAULT, 0,

&st, NULL, _tcschr(szBuf, TEXT('\0')),

(int)(_countof(szBuf) - _tcslen(szBuf)));

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

MessageBox(NULL, szBuf, TEXT("Timer went off at…"), MB_OK);

}

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

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

ми и 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 и в «тревожном» состоянии. Когда таймер перейдет в свободное состояние, поток пробудится, что выведет его из «тревожного» состояния, и вызова АРС-функции не последует. Правда, АРС-функции редко используются совместно с ожидаемыми таймерами, так как всегда можно дождаться перехода таймера в свободное состояние, а затем сделать то, что нужно.

Глава 9. Синхронизация потоков с использованием объектов ядра.docx 305

И еще кое-что о таймерах

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

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

Конечно, очень мило, что таймеры поддерживают АРС-очереди, но большинство современных приложений использует не APC, а порты завершения вводавывода. Как-то раз мне понадобилось, чтобы один из потоков в пуле (управляемом через порт завершения ввода-вывода) пробуждался по таймеру через определенные интервалы времени. К сожалению, такую функциональность ожидаемые таймеры не поддерживают. Для решения этой задачи мне пришлось создать отдельный поток, который всего-то и делал, что настраивал ожидаемый таймер и ждал его освобождения. Когда таймер переходил в свободное состояние, этот поток вызывал PostQueuedCompletionStatus, передавая соответствующее уведомление потоку в пуле.

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

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

Если в ответ на срабатывание таймера вы собираетесь выполнять какието операции, связанные с пользовательским интерфейсом, то, по-видимому, будет легче структурировать код под таймеры User, поскольку применение ожидаемых таймеров требует от потоков ожидания не только сообщений, но и объектов ядра. (Если у вас есть желание переделать свой код, используйте функцию MsgWaitForMultipleObjects, которая как раз и рассчитана на

Соседние файлы в предмете Программирование на C++