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

лекции / Shchupak_Yu._Win32_API_Razrabotka_prilozheniy_dlya_Windows

.pdf
Скачиваний:
0
Добавлен:
11.02.2026
Размер:
13.15 Mб
Скачать

Обмен данными между процессами

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.

Обмен данными между процессами

483

 

 

Далее работа программы управляется прерываниями от таймера. Обрабатывая сообщения WM_TIMER, приложение проверяет с помощью функции WaitForSingleObject, свободен ли сервер. Если да, то в разделяемую память записыва" ется «рабочий запрос», представляющий собой С"строку с номером запроса. Если нет, то приложение ждет освобождения сервера.

После успешной записи «рабочего запроса» в разделяемую память программа переводит в свободное состояние объект"событие hEvtRecToServ. Именно это собы" тие ожидает сервер. Затем клиент переходит к ожиданию события hEvtServIsDone, которое свидетельствует о том, что сервер выполнил запрос. Как только указанное событие освобождается, полученный от сервера ответ извлекается из разделяемой памяти и выводится в окно приложения.

На рис. 9.4 показаны в работе сервер ServerApp и три клиента ClientApp.

Рис. 9.4. Сервер ServerApp обслуживает трех клиентов ClientApp

Заметим, что с целью сокращения объема текста в приведенных программах опущены те проверки кодов возврата, которые должны производиться после вы" зова функций Create… и Open… . В реальных приложениях вы, конечно, должны предусмотреть подобные проверки.

Не забывайте освобождать ресурсы

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

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. В неко

488

Глава 10. Таймеры и время

 

 

торых случаях может оказаться полезной пара функций, работающих с информа цией о часовом поясе, — GetTimeZoneInformation и SetTimeZoneInformation.

Системное время считывается с часов реального времени, встроенных в компь ютер и имеющих автономное питание, в момент запуска Windows. Затем опера ционная система обеспечивает приращения системного времени, используя пре рывания системного таймера, аналогично управлению временем Windows. Таким образом, точность измерения времени с помощью GetSystemTime также определя ется разрешением системного таймера.

Измерение малых временных интервалов

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

Функции считывания текущего времени GetTickCount и GetSystemTime, с кото рыми мы уже познакомились, теоретически, могут быть использованы для про филировки1 некоторого фрагмента программы. Например, можно использовать следующий фрагмент кода:

int t1 = GetTickCount();

// ... профилируемый участок кода int t2 = GetTickCount();

int elapsedTime = t2 – t1;

Но разрешение этих функций определяется разрешением системного таймера и для нашего компьютера, например, равно 15,625 мс. Это очень большая величи на, так как при тактовой частоте процессора порядка 2 ГГц за это время могут быть выполнены сотни тысяч машинных команд.

Для более точных измерений малых интервалов следует использовать либо счетчик монитора производительности (QueryPerformanceCounter), либо счетчик меток реального времени, доступ к которому реализован при помощи ассемб лерной команды rdtsc.

Использование счетчика монитора производительности

Win32 API содержит две функции для работы с высокоточным счетчиком мони тора производительности. Функция QueryPerformanceFrequency возвращает коли чество приращений в секунду для этого счетчика:

BOOL QueryPerformanceFrequency(LARGE_INTEGER* lpFrequency);

Если счетчик монитора производительности не поддерживается системой, то функция вернет нулевое значение. Если счетчик все же поддерживается, то его частота записывается в 64 разрядную переменную типа LARGE_INTEGER по адресу

1 Профилировкой называют измерение производительности как всей программы в целом, так и от дельных ее фрагментов.

Измерение малых временных интервалов

489

 

 

lpFrequency. Тип LARGE_INTEGER определен как объединение структуры из двух 32 разрядных полей LowPart, HighPart и 64 разрядного поля QuadPart:

typedef union _LARGE_INTEGER { struct {

DWORD LowPart;

LONG HighPart;

};

LONGLONG QuadPart; } LARGE_INTEGER;

Если компилятор не имеет встроенной поддержки 64 разрядных знаковых це лых величин, то можно использовать поля LowPart (младшие 32 разряда) и HighPart (старшие 32 разряда).

Вторая функция, QueryPerformanceCounter, возвращает текущее значение счет чика монитора производительности:

BOOL QueryPerformanceCounter(LARGE_INTEGER* lpPerformanceCount);

записывая его по адресу lpPerformanceCount.

Для профилировки некоторого участка кода необходимо определить пере менные

LARGE_INTEGER freq;

LARGE_INTEGER count_t1;

LARGE_INTEGER count_t2;

После этого следует получить значение частоты счетчика freq:

QueryPerformanceFrequency(&freq);

Сама профилировка выглядит следующим образом:

QueryPerformanceCounter(&count_t1);

//... профилируемый участок кода QueryPerformanceCounter(&count_t2);

double dt = count_t2.QuadPart - count_t1.QuadPart; double elapsedTime = 1000 * dt / freq.QuadPart;

//время выполнения в миллисекундах

Какова разрешающая способность счетчика монитора производительности? Она определяется его частотой freq.QuadPart. Наши эксперименты на разных ком пьютерах показали, что независимо от тактовой частоты процессора часто та freq.QuadPart равна 3 579 545 тиков в секунду. Таким образом, промежуток времени между двумя тиками равен 1/3579545 с, что составляет 0,279 мкс или 279 нс.

Сравните эту разрешающую способность (0,279 мкс) с разрешающей способ ностью функции GetTickCount (15 625 мкс), чтобы почувствовать разницу.

Однако в процессорах семейства Intel Pentium1 появилась возможность еще более точного измерения малых интервалов времени, основанная на использова нии счетчика меток реального времени.

Использование команды RDTSC

Начиная с Pentium III, процессоры этого семейства содержат доступный для про граммистов счетчик меток реального времени TSC (Time Stamp Counter). Это

1 А также его клонов, таких, как, например, Intel Celeron, AMD, VIA и т. д.

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.

Прежде чем сделать это, обратим ваше внимание на то, что профилировка не больших фрагментов программы сопряжена с рядом серьезных и не всегда оче видных проблем, незнание которых может привести к грубым ошибкам. Подробно