лекции / Shchupak_Yu._Win32_API_Razrabotka_prilozheniy_dlya_Windows
.pdfОбмен данными между процессами |
481 |
|
|
Листинг 9.5 (продолжение)
// Дескрипторы разделяемых объектов-событий
hEvtRecToServ = OpenEvent(EVENT_ALL_ACCESS, FALSE, eventName); hEvtServIsFree = OpenEvent(EVENT_ALL_ACCESS, FALSE, "ServerIsFree"); hEvtServIsDone = OpenEvent(EVENT_ALL_ACCESS, FALSE, "ServerIsDone");
// Открыть файл, проецируемый в память
hFileMap = OpenFileMapping(FILE_MAP_ALL_ACCESS, FALSE, "SharedData");
SetWindowText(hWnd, eventName); isLinkToServer = TRUE; InvalidateRect(hWnd, NULL, TRUE);
SetTimer(hWnd, TIMER_ID, TIMER_PERIOD, NULL); break;
case WM_TIMER:
// Отправка "рабочих запросов" и ожидание ответа от сервера
// Ожидание освобождения сервера
dw0 = WaitForSingleObject(hEvtServIsFree, TIMER_PERIOD / 2); switch (dw0) {
case WAIT_OBJECT_0:
// Отображаем проекцию файла на адресное пространство процесса pView = MapViewOfFile(hFileMap, FILE_MAP_WRITE, 0, 0, 0);
//Записываем содержание "рабочего запроса" sprintf((PTSTR)pView, "%d\0", ++count);
//Сообщение для вывода в окно приложения
sprintf(msgSended, "Запрос к серверу: \t\t%s", (PTSTR)pView); UnmapViewOfFile(pView);
InvalidateRect(hWnd, NULL, FALSE);
SetEvent(hEvtRecToServ); // освобождаем событие hEvtRecToServ
// Ожидание события "Сервер выполнил запрос"
dw1 = WaitForSingleObject(hEvtServIsDone, TIMER_PERIOD / 2);
switch (dw1) { |
|
case WAIT_OBJECT_0: |
|
// Опять отображаем проекцию файла |
|
pView = MapViewOfFile(hFileMap, |
FILE_MAP_READ, 0, 0, 0); |
//Извлекаем содержание ответа сервера sprintf(msgReceived, "Ответ от сервера: \t\t%s",
(PTSTR)pView);
UnmapViewOfFile(pView); bServerIsDone = TRUE;
//освобождаем событие "Сервер свободен" SetEvent(hEvtServIsFree); InvalidateRect(hWnd, NULL, FALSE); break;
case WAIT_TIMEOUT: return 0; case WAIT_FAILED:
MessageBox(hWnd, "Ошибка ожидания hEvtServIsDone", "ClientApp", MB_OK);
return 0;
}
case WAIT_TIMEOUT: return 0;
482 Глава 9. Многозадачность
case WAIT_FAILED:
MessageBox(hWnd, "Ошибка ожидания hEvtServIsFree", "ClientApp", MB_OK);
return 0;
}
break;
case WM_PAINT:
hDC = BeginPaint(hWnd, &ps);
if (isLinkToServer) {
sprintf(text, "Установлена связь с сервером через событие %s", eventName);
TextOut(hDC, 20, 20, text, strlen(text)); TabbedTextOut(hDC, 20, 40, msgSended, strlen(msgSended), 0,
NULL, 20);
}
if (bServerIsDone) { bServerIsDone = FALSE;
TabbedTextOut(hDC, 20, 60, msgReceived, strlen(msgReceived), 0, NULL, 20);
}
EndPaint(hWnd, &ps); break;
case WM_DESTROY: UnmapViewOfFile(pView); CloseHandle(hFileMap); PostQuitMessage(0); break;
default:
return DefWindowProc(hWnd, uMsg, wParam, lParam);
}
return 0;
}
//////////////////////////////////////////////////////////////////////
Когда приложение запущено, работа клиента начинается после того, как пользователь выполнит команду меню Link to server. Обрабатывая эту команду (case IDM_LINK), программа посылает серверу сообщение WM_COPYDATA, содержа" щее указатель на строку request.
Получив ответ"уведомление от сервера также через сообщение WM_COPYDATA, клиент выполняет следующие действия:
извлекает имя разделяемого объекта"события eventName;
открывает разделяемые объекты"события, получая дескрипторы hEvtRecToServ, hEvtServIsFree, hEvtServIsDone (событие hEvtRecToServ в дальнейшем клиент ис" пользует, чтобы информировать сервер о готовности «рабочего запроса»);
открывает существующий объект «проекция файла» с именем SharedData (сер" вер должен быть запущен раньше клиента);
заменяет текст заголовка своего окна строкой eventName;
запускает стандартный таймер с периодом TIMER_PERIOD.
484 |
Глава 9. Многозадачность |
|
|
чете на предоставление сервисных услуг N клиентам, а реализация каждой услуги требует создания отдельного процесса. По всей видимости, вы объявите массивы структур STARTUPINFO и PROCESS_INFORMATION:
# define N 1000 STARTUPINFO si[N]; PROCESS_INFORMATION pi[N];
int iProc = –1; // индекс обслуживающего процесса
Теперь, получив запрос от клиента, сервер может создать отдельный процесс для его обработки:
iProc++; |
|
|
|
ZeroMemory( |
&pi[iProc], |
sizeof(pi[iProc]) |
); |
ZeroMemory( |
&si[iProc], |
sizeof(si[iProc]) |
); |
si[iProc].cb |
= sizeof(si); |
|
|
CreateProcess( NULL, "ServProcess.exe", NULL, NULL, FALSE, |
|||
0, NULL, |
NULL, &si[iProc], &pi[iProc]); |
||
Здесь iProc — текущий индекс для создаваемого процесса. Мы опускаем под" робности, связанные с управлением этим индексом. Например, при достижении максимально допустимого значения N–1 с ним надо что"то делать (в простейшем алгоритме следующим значением iProc должен быть 0).
Но в дальнейшем сервер должен проверять состояние запущенных ранее про" цессов и в случае их завершения сразу же освобождать задействованные ресурсы. Такую проверку можно осуществить, например, с помощью следующего кода, ко" торый вызывается по прерыванию от таймера:
DWORD dwExitCode;
for (i = 0; i < N; ++i) { if (pi[i].hProcess) {
GetExitCodeProcess (pi[i].hProcess, &dwExitCode); if (dwExitCode != STILL_ACTIVE) {
CloseHandle(pi[i].hThread);
CloseHandle(pi[i].hProcess); pi[i].hProcess = 0;
}
}
}
Обработка ошибок здесь опущена. Вызов функции GetExitCodeProcess позволя" ет определить, завершился ли процесс с дескриптором pi[i].hProcess? Если да, то корректное освобождение ресурсов обеспечивается вызовами функции CloseHandle для дескриптора основного потока и дескриптора завершившегося процесса.
Когда многопоточность реально полезна?
«Потоки — вещь невероятно полезная, когда ими пользуются с умом» — это цита" та из Джеффри Рихтера [5]. В то же время бездумное применение многопоточно" сти в любой программе может привести к получению совершенно неожиданных результатов. Например, вы решили повысить производительность приложения за счет распараллеливания вычислений и сделали для этого несколько потоков. Возможно, что на многопроцессорной машине программа действительно станет работать быстрее, но на обычном однопроцессорном персональном компьютере
Когда многопоточность реально полезна? |
485 |
|
|
ее характеристики ухудшатся, так как система будет тратить процессорное время на переключение между потоками.
Приведем несколько примеров известных приложений, работающих в много" поточном режиме:
Текстовые процессоры принимают ввод от пользователя, проверяют его на ор" фографические ошибки и печатают в фоновом режиме.
Windows Explorer может копировать файлы из одной папки в другую и одновре" менно предоставляет пользователю другие функции. Например, можно просмат" ривать содержимое других папок или запускать любое другое приложение.
Веб"браузеры способны взаимодействовать с серверами в фоновом режиме. Благодаря этому пользователь может перейти на другой сайт, не дожидаясь, когда будет получена вся информация с текущего ресурса.
Во всех этих приложениях имеется разделение на несколько относительно независимых задач, которые могут выполняться разными потоками, имеющими различный приоритет. В примере с веб"браузером выделение ввода"вывода в от" дельный поток обеспечивает «отзывчивость» пользовательского интерфейса приложения даже при интенсивной передаче данных.
Потоки, используемые в приложениях Win32, могут быть двух типов:
потоки пользовательского интерфейса (user interface threads);
рабочие потоки (worker threads).
Потоки первого типа позволяют работать с ними при помощи механизма сооб" щений и имеют в своем составе оконную процедуру. Типичным примером такого потока является первичный поток Windows"приложения, содержащий кроме вход" ной функции WinMain еще и оконную функцию WndProc.
Потоки второго типа, напротив, сообщения обрабатывать не могут и применя" ются обычно для фонового выполнения задач. В качестве примера можно привести поток с входной функцией ThreadFunc в рассмотренном выше приложении ServerApp.
Чаще всего приложению достаточно иметь один поток пользовательского ин" терфейса для взаимодействия с пользователем. Необходимость использования вторичных рабочих потоков определяется, исходя из решаемой задачи. Напри" мер, если ваша программа должна принимать в фоновом режиме данные из двух COM"портов, стоит организовать два рабочих потока для решения этих задач.
Несколько потоков пользовательского интерфейса в одном процессе можно обнаружить в таких приложениях, как Windows Explorer. Оно создает отдельный поток для каждого окна папки. Кроме параллельного решения различных задач, о котором говорилось выше, такая архитектура повышает надежность приложе" ния. Если какая"то ошибка в Explorer приводит к краху одного из его потоков, то прочие потоки остаются работоспособны.
Не забывайте, что если вы решили сделать ваше приложение многопоточным, то необходимо очень тщательно продумать вопросы синхронизации работы потоков.
В заключение следует привести еще одну цитату из Джеффри Рихтера: «…Мно" гопоточность следует использовать разумно».
486 |
Глава 10. Таймеры и время |
10 Таймеры и время
При решении некоторых задач программа должна отслеживать текущее время или выполнять какие либо действия с определенной периодичностью. Например, эта проблема возникает в приложениях, имитирующих аппаратуру, работающую в реальном масштабе времени, в игровых и мультимедийных приложениях, а так же при проведении различных тестов. Кроме того, иногда требуется отладить кри тичные ко времени исполнения фрагменты кода, для чего нужен «хронометр» с высокой разрешающей способностью.
Win32 API содержит как функции для измерения текущего времени, так и функ ции для создания виртуальных таймеров — устройств, извещающих приложение об истечении заданного интервала времени. Для успешного применения этих про граммных средств необходимо учитывать их разрешающую способность и потен циальную точность измерения.
Важно понимать, что многозадачная операционная система Windows не являет ся системой реального времени, поэтому любой виртуальный таймер в Windows не может гарантировать какой бы то ни было фактической точности отсчета временно го интервала. Ведь в любой момент времени система может прервать выполнение вашего приложения, чтобы дать возможность поработать другому приложению (про стой, вызванный прерыванием, чаще всего длится от 1 до 30 мс1). Вероятность та ких прерываний тем ниже, чем меньше рассматриваемый временной интервал и чем меньше других программ работает одновременно с вашим приложением. В то же время, как показывают эксперименты, мультимедийный таймер Windows обес печивает вполне приемлемую фактическую точность отсчета временных интерва лов для многих задач.
В этой главе кроме функций Win32 API мы рассмотрим также использование ассемблерной команды rdtsc для реализации «хронометра» с высокой разрешаю щей способностью.
Время Windows
Время Windows — это количество миллисекунд, прошедших с момента старта опе рационной системы. Этот формат времени поддерживается для обратной совме
1Указанная здесь нижняя граница весьма приблизительна и относится к тому случаю, когда систем ный диспетчер потоков выделяет очередной квант этому же приложению.
Системное время |
487 |
|
|
стимости с 16 разрядными версиями Windows. Время Windows хранится в виде 32 разрядного целого числа без знака, которое сбрасывается в нулевое значение после того, как Windows проработает примерно 49,7 дней.
Операционная система управляет временем Windows через прерывания сис темного таймера, добавляя к текущему значению времени Windows приращение, равное периоду работы системного таймера. Кроме того, система периодически синхронизирует время Windows с показаниями часов реального времени, то есть с системным временем, рассматриваемым ниже.
Системный таймер Windows — это программное устройство, находящееся под управлением операционной системы (в отличие от аппаратного таймера, с кото рым работали программы под управлением MS DOS).
Обычно период прерываний системного таймера составляет 10 или около 15 мс в зависимости от аппаратной платформы. Точное значение этого периода, называе мое также разрешением системного таймера, можно получить с помощью функции GetSystemTimeAdjustment, имеющей следующий прототип:
BOOL GetSystemTimeAdjustment(PDWORD lpTimeAdjustment,PDWORD lpTimeIncrement, PBOOL lpTimeAdjustmentDisabled);
Эта функция предоставляет информацию, относящуюся к синхронизации сис темного времени и времени Windows. При этом значение, возвращаемое через второй параметр, как раз равно периоду прерывания системного таймера, выра женному в 100 наносекундных единицах.
Например, для компьютера, на котором тестировались программы, приводи мые в данной книге (с процессором Intel Celeron CPU 2,0 ГГц и операционной системой Microsoft Windows 2000), разрешение системного таймера, полученное с помощью функции GetSystemTimeAdjustment, равно 15,625 мс.
Для получения текущего значения времени Windows предназначена функция GetTickCount. Функция возвращает число миллисекунд, прошедших с момента стар та системы. Точность этого измерения определяется разрешающей способностью системного таймера.
Системное время
Системное время в Windows содержит информацию о текущих дате и времени и представляет собой так называемое UTC время (Universal Time Coordinated). Время в формате UTC основывается на среднем времени по Гринвичу. Системное время может быть получено при помощи функции GetSystemTime:
VOID GetSystemTime(LPSYSTEMTIME lpSystemTime);
Функция записывает результат в структуру типа SYSTEMTIME, адрес которой задается параметром lpSystemTime. Структура типа SYSTEMTIME содержит поля для года, месяца, дня недели, дня, часов, минут, секунд и миллисекунд.
Так как системное время отсчитывается по Гринвичу, то, скорее всего, оно не совпадает с местным временем, которое отображается на панели задач. Получить значение местного времени можно при помощи функции GelLocalTime, которая воз вращает информацию в том же формате, что и функция GetSystemTime. Если вы считаете, что ваше приложение может изменять системное время, то это можно осуществить при помощи вызова функции SetSystemTime или SetLocalTime. В неко
490 |
Глава 10. Таймеры и время |
|
|
64 разрядный регистр, содержимое которого инкрементируется с каждым тактом процессорного ядра. Каждый раз при аппаратном сбросе (сигналом RESET) от счет в счетчике TSC начинается с нуля. Разрядность регистра обеспечивает от счет времени без переполнения в течение сотен лет.
Команда rdtsc (read time stamp counter) возвращает количество тактов с мо мента запуска процессора, помещая результат в пару регистров общего назначе ния EDX:EAX. Функция на языке C++ может использовать эту команду следую щим образом:
unsigned __int64 GetCycleCount(void) { _asm rdtsc
}
Если ваш компилятор не «понимает» команды rdtsc, используйте ее машинное представление:
_asm _emit 0x0F _asm _emit 0x31
Так как частота работы у разных процессоров может различаться и колеблется в настоящее время от 60 МГц до 3 ГГц, то для измерения временных интервалов счетчик нужно отградуировать с помощью стандартных функций измерения вре мени ОС, например при помощи функции Sleep. Эта функция будет рассматри ваться в разделе «Программирование задержек в исполнении кода».
Следующий фрагмент кода иллюстрирует механизм градуировки счетчика TSC:
unsigned __int64 t_start; unsigned __int64 t_stop; t_start = GetCycleCount(); Sleep(1000);
t_stop = GetCycleCount() - t_start - overhead; double nCyclePer1microSec = t_stop / 1000000.;
Здесь функция Sleep приостанавливает выполнение потока на 1000 мс. Пока зания счетчика TSC фиксируются определенной выше функцией GetCycleCount перед вызовом функции Sleep и после возврата из нее. Разница этих показаний, запоминаемая в переменной t_stop, показывает количество машинных тактов, про шедших за одну секунду. Переменная overhead учитывает «накладные расходы», связанные с выполнением функции GetCycleCount, и должна быть вычислена заб лаговременно. Получив величину t_stop, можно вычислять различные калибро вочные коэффициенты, например коэффициент nCyclePer1microSec, определяющий, сколько тактов содержится в одной микросекунде.
После вычисления калибровочных коэффициентов профилировка некоторо го участка программы осуществляется следующим образом:
t_start = GetCycleCount();
// ... профилируемый участок кода
t_stop = GetCycleCount() - t_start - overhead;
double elapsedTime = t_stop / nCyclePer1microSec; // время выполнения в микросекундах
Однако мы до сих пор не пояснили, что скрывается за таинственной поправоч ной величиной overhead.
Прежде чем сделать это, обратим ваше внимание на то, что профилировка не больших фрагментов программы сопряжена с рядом серьезных и не всегда оче видных проблем, незнание которых может привести к грубым ошибкам. Подробно
