лекции / Shchupak_Yu._Win32_API_Razrabotka_prilozheniy_dlya_Windows
.pdf
Программирование задержек в исполнении кода |
501 |
|
|
Чтобы убедиться в этом, раскомментируйте в листинге 10.3 инструкции, по меченные номерами #1 и #3, и проведите испытания повторно. Скорее всего, вы получите результаты, близкие к тем, которые показаны в табл. 10.2.
Таблица 10.2. Реальная задержка, обеспечиваемая функцией Sleep после вызова timeBeginPeriod(1)
SLEEP_TIME, ìñ |
Реальная задержка, мс |
|
|
|
|
|
|
|
Минимальная |
Максимальная |
Средняя |
|
|
|
|
1 |
1,66 |
4,33 |
1,98 |
2 |
2,78 |
3,07 |
2,93 |
3 |
3,75 |
4,06 |
3,91 |
5 |
5,38 |
7,11 |
5,87 |
10 |
10,26 |
12,93 |
10,78 |
20 |
20,21 |
20,80 |
20,51 |
100 |
100,20 |
100,95 |
100,57 |
1000 |
999,85 |
1001,81 |
1000,34 |
|
|
|
|
Хотя в области малых значений (менее 10 мс) реальная задержка получается с существенной относительной погрешностью, все же эти результаты намного предпочтительнее тех, которые были показаны в табл. 10.1.
Использование метода uDelay класса KTimer
Если вам нужна задержка в микросекундном диапазоне, то функция Sleep ста новится бесполезной. Возможны разные варианты решения этой проблемы, один из них — вызов метода uDelay класса KTimer. Чтобы посмотреть, как он работает, можно модифицировать программу SleepTest (см. листинг 10.3) следующим об разом.
Добавьте макрос
#define U_SEC 1 // задержка в микросекундах
и определение еще одного объекта класса KTimer:
KTimer timer1;
Затем замените код профилировки, содержащийся в блоке обработки сообще ния WM_CREATE, на следующий фрагмент:
timer1.SetUnit(Usec);
timer.SetUnit(Usec);
for (i = 0; i < N; ++i) { timer.Start(); timer1.uDelay(U_SEC);
realTimeInterval[i] = timer.GetTime();
}
После этого проведите испытания программы для разных значений U_SEC. На нашем компьютере были получены результаты, показанные в табл. 10.3.
СОВЕТ
Не забывайте, что реализация класса KTimer основана на использовании ассемблерной команды rdtsc, которая поддерживается процессорами модельных рядов не ниже Pentium III. Если вам нужно обеспечить работоспособность приложения на более старых аппаратных платформах, рекомендуем для реализации малых задержек использовать приведенный ниже класс QTimer.
502 Глава 10. Таймеры и время
Таблица 10.3. Реальная задержка, обеспечиваемая методом uDelay класса KTimer
U_SEC, ìêñ |
Реальная задержка, мкс |
|
|
|
|
|
|
|
Минимальная |
Максимальная |
Средняя |
|
|
|
|
1 |
1,20 |
1,25 |
1,21 |
2 |
2,19 |
2,22 |
2,20 |
5 |
5,14 |
5,18 |
5,15 |
10 |
10,14 |
10,16 |
10,15 |
50 |
50,20 |
50,29 |
50,27 |
100 |
100,15 |
100,24 |
100,23 |
1000 |
1000,15 |
1679,37 |
1007,03 |
|
|
|
|
Класс QTimer
Класс QTimer имеет интерфейс, совпадающий с интерфейсом класса KTimer. Но ре ализация его методов основана на использовании счетчика монитора производи тельности. В листинге 10.4 приведено определение данного класса.
Листинг 10.4. Класс QTimer
//////////////////////////////////////////////////////////////////////
// QTimer.h #ifndef QTIMER_H #define QTIMER_H
#pragma warning(disable : 4035)
typedef enum { Msec, Usec, Ns_100, Ns_10, Nsec } TimeUnit; class QTimer {
LARGE_INTEGER freq; LARGE_INTEGER count_beg; LARGE_INTEGER count_end;
LONGLONG ll_c_beg; LONGLONG ll_c_end; long d_tic;
double dt; DWORD lowPart; LONG highPart;
// коэффициенты-делители double nCyclePer1nanoSec; double nCyclePer10nanoSec; double nCyclePer100nanoSec; double nCyclePer1microSec; double nCyclePer1milliSec;
double divizor; // текущий делитель public:
QTimer(void) { // Калибровка
timeBeginPeriod(1);
Sleep(20);
BOOL success |
= QueryPerformanceFrequency(&freq); |
if (!success) |
продолжение |
Программирование задержек в исполнении кода |
503 |
|
|
Листинг 10.4. (продолжение)
MessageBox(NULL, "Счетчик QueryPerformance не поддерживается", 0, MB_OK | MB_ICONSTOP);
lowPart = freq.LowPart; highPart = freq.HighPart;
// Калибровка делителей |
|
nCyclePer1nanoSec |
= freq.LowPart / 1000000000.; |
nCyclePer10nanoSec |
= freq.LowPart / 100000000.; |
nCyclePer100nanoSec = freq.LowPart / 10000000.; |
|
nCyclePer1microSec |
= freq.LowPart / 1000000.; |
nCyclePer1milliSec |
= freq.LowPart / 1000.; |
timeEndPeriod(1);
divizor = nCyclePer1milliSec; // делитель по умолчанию
}
void SetUnit(TimeUnit unit) { switch (unit) {
case Msec: divizor = nCyclePer1milliSec; break;
case Usec: divizor |
= nCyclePer1microSec; |
break; |
|
case |
Nsec: divizor |
= nCyclePer1nanoSec; |
break; |
case |
Ns_10: divizor = nCyclePer10nanoSec; |
break; |
|
case Ns_100: divizor = nCyclePer100nanoSec; break;
}
}
inline void Start(void) { QueryPerformanceCounter(&count_beg); }
inline long GetTick(void) { QueryPerformanceCounter(&count_end); ll_c_beg = count_beg.QuadPart; ll_c_end = count_end.QuadPart;
d_tic = ll_c_end - ll_c_beg; return d_tic;
}
inline double GetTime(void) { QueryPerformanceCounter(&count_end); ll_c_beg = count_beg.QuadPart; ll_c_end = count_end.QuadPart;
d_tic = ll_c_end - ll_c_beg;
return d_tic / divizor;
}
inline int GetTickPerUsec(void) { return (int)nCyclePer1microSec; }
inline void uDelay(int uSec) { int tElapsed = 0; SetUnit(Usec);
Start();
while ( tElapsed < uSec) tElapsed = (int)GetTime();
}
504 |
Глава 10. Таймеры и время |
|
|
};
#endif /* QTIMER_H */
//////////////////////////////////////////////////////////////////////
Поскольку класс QTimer имеет такой же интерфейс, что и класс KTimer, исполь зование его объектов не отличается от использования объектов класса KTimer.
Стандартный таймер
Стандартный таймер в Windows — это устройство, периодически уведомляющее приложение о завершении заданного интервала времени. Вы можете присоеди нить стандартный таймер к своей программе с помощью функции SetTimer:
UINT_PTR SetTimer( |
|
HWND hWnd, |
// дескриптор окна |
UINT_PTR nIDEvent, |
// идентификатор таймера |
UINT uElapse, |
// интервал в миллисекундах |
TIMERPROC lpTimeProc |
// адрес функции - обработчика сообщения WM_TIMER |
); |
|
Первый параметр, hWnd, содержит дескриптор окна, ассоциированного с дан ным таймером. Если этот параметр равен NULL, то с таймером не связывается ни какое окно и сообщения от таймера будут приходить в специально созданную для этого функцию.
Второй параметр, nIDEvent, позволяет указывать идентификатор таймера, ко торым может быть произвольное целое число (но не нуль). Если программа ис пользует более одного таймера, то рекомендуется определить идентификаторы таймеров в виде именованных констант, например, с помощью типа enum или ди рективы #define. Это улучшает читаемость кода программы. Если параметр hWnd равен NULL, то параметр nIDEvent игнорируется.
Третий параметр, uElapse, задает интервал, который может находиться в преде лах (теоретически) от 1 до 4 294 967 295 мс, что составляет около 50 дней. Это значение определяет темп, с которым Windows будет посылать вашей программе сообщения WM_TIMER. Сообщения WM_TIMER направляются либо оконной проце дуре для окна hWnd, если параметр lpTimeProc равен NULL, либо функции обратного вызова с адресом lpTimeProc — в противном случае.
Если на месте hWnd указано значение NULL, то возвращаемое функцией значе ние является идентификатором созданного таймера. В любом случае функция SetTimer возвращает нулевое значение, если она не смогла создать таймер.
Если таймер по истечении некоторого времени больше не нужен, то рекомен дуется его уничтожить, вызвав функцию KillTimer:
BOOL KillTimer(HWND hWnd, UINT_PTR uIDEvent);
В параметре hWnd указывается дескриптор окна, с которым был связан таймер. Это значение должно совпадать со значением hWnd, указанным при вызове функ ции SetTimer.
Второй параметр содержит идентификатор уничтожаемого таймера. На его месте используется либо идентификатор, указанный при создании таймера, если он создавался для окна, либо значение, возвращенное функцией SetTimer (для тай мера, имеющего собственную функцию обработки сообщений).
Стандартный таймер |
505 |
|
|
Первый способ использования стандартных таймеров
В первом способе таймер подключается к окну, заданному параметром hWnd при вызове функции SetTimer, а параметр lpTimeProc получает значение NULL. В этом случае функция окна, к которому подключен таймер, будет получать сообщения от таймера с кодом WM_TIMER.
Допустим, что вашей программе требуется два таймера. Один таймер имеет период срабатывания в 1 с, а другой обладает периодом срабатывания в 1 мин. Вы могли бы сначала определить идентификаторы таймеров с помощью инструкций
#define:
#define TIMER_SEC 1 #define TIMER_MIN 2
Затем либо в теле функции WinMain, либо в блоке обработки сообщения WM_CREATE оконной процедуры нужно вызвать функции для создания двух тай меров:
SetTimer(hWnd, TIMER_SEC, 1000, NULL);
SetTimer(hWnd, TIMER_MIN, 60000, NULL);
Поскольку параметр wParam сообщения WM_TIMER содержит идентификатор таймера, то логика обработки сообщения WM_TIMER в оконной процедуре обычно выглядит примерно так:
case WM_TIMER: switch (wParam) {
case TIMER_SEC:
…// обработка одного сообщения в секунду break;
case TIMER_MIN:
…// обработка одного сообщения в минуту break;
}
break;
После того как эти таймеры перестанут быть нужными для работы приложе ния, рекомендуется их уничтожить, вызвав следующие инструкции:
KillTimer(hWnd, TIMER_SEC);
KillTimer(hWnd, TIMER_MIN);
Отметим, что на практике реальный интервал генерации сообщений WM_TIMER может значительно отличаться от заданного вами значения для параметра uElapse, особенно для малых значений. Здесь необходимо учитывать следующие недоку ментированные особенности функционирования стандартного таймера. Во пер вых, реальный интервал не может быть меньше разрешения системного таймера. Во вторых, реальный интервал всегда является некоторым приближением к бли жайшему большему значению, кратному разрешению системного таймера. Напри мер, если системный таймер имеет разрешение 15,625 мс, то ряд допустимых зна чений, в окрестностях которых реализуются значения реального интервала, имеет вид 15,625 мс, 31,25 мс, 46,875 мс, 62,5 мс и т. д.
Приведем результаты экспериментов по измерению реального интервала ге нерации сообщений WM_TIMER, проведенных на компьютере с процессором Intel
506 |
Глава 10. Таймеры и время |
|
|
Celeron CPU 2,0 ГГц и операционной системой Microsoft Windows 2000 (разре шение системного таймера 15,625 мс). Измерения осуществлялись путем фикса ции времени поступления в оконную процедуру сообщения WM_TIMER для тыся чи последовательных тиков стандартного таймера. Тестовая программа приведена в листинге 10.5.
Листинг 10.5. Проект SetTimer1Test
//////////////////////////////////////////////////////////////////////
// SetTimer1Test.cpp #include <windows.h> #include <stdio.h> #include "KWnd.h" #include "KTimer.h"
KTimer timer; #define ID_TIMER 1
#define TIME_PERIOD 1 #define N 1000
double realTimeInterval[N];
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); //====================================================================
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
MSG msg;
KWnd mainWnd("SetTimer1 - test", hInstance, nCmdShow, WndProc);
while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg);
}
return msg.wParam;
}
//==================================================================== LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
HDC hDC; PAINTSTRUCT ps; static int x, y; char text[200]; int i;
double minT, maxT, amount; static int count = -1;
switch (uMsg)
{
case WM_CREATE:
SetTimer(hWnd, ID_TIMER, TIME_PERIOD, NULL);
break; |
|
case WM_TIMER: |
|
if (count >= 0 && count < N) |
|
realTimeInterval[count] = |
timer.GetTime(); |
timer.Start(); |
|
count++; |
продолжение |
Стандартный таймер |
509 |
|
|
Сама установка таймера выглядит следующим образом:
idTimer = SetTimer(NULL, NULL, TIME_PERIOD, (TIMERPROC)TimerProc);
где TIME_PERIOD — именованная константа, задающая интервал в миллисекундах. После окончания работы с таймером он уничтожается вызовом функции
KillTimer(NULL, idTimer);
В заключение следует отметить, что принципиальных различий в функциони ровании стандартных таймеров, созданных первым или вторым способом, не на блюдается. И в том, и в другом случаях таймеры генерируют сообщения WM_TIMER, поступающие в очередь сообщений приложения, а проблемы, связанные с обра боткой этого сообщения, уже рассматривались выше.
Хотя стандартный таймер Windows не отличается высокой точностью и на дежностью, тем не менее, приложения могут использовать его сервис, если от клонения в периоде генерации сообщений таймера не очень важны для решаемой задачи. В противном случае рекомендуется прибегать к услугам мультимедийного таймера.
Мультимедийный таймер
Для использования в вашем приложении мультимедийного таймера необходимо подключить к проекту мультимедийную библиотеку winmm.lib1 и добавить в на чало файла, в котором вызываются функции библиотеки, следующую директиву:
#include <Mmsystem.h>
Содержащаяся в библиотеке функция timeGetDevCaps позволяет узнать поддер живаемое системой разрешение мультимедийного таймера. Для этого определите переменную структурного типа
TIMECAPS tc;
и вызовите функцию
timeGetDevCaps(&tc, sizeof(TIMECAPS));
В результате этого вызова поля tc.wPeriodMin и tc.wPeriodMax будут содержать минимальное и максимальное разрешения в миллисекундах, поддерживаемые для мультимедийного таймера. Для нашего компьютера, например, были получены значения 1 мс и 1 000 000 мс соответственно.
Мультимедийная библиотека содержит функции timeBeginPeriodи timeEndPeriod, предназначенные для установки и отмены конкретного разрешения мультимедий ного таймера. Величина разрешения в миллисекундах передается в виде парамет ра в обеих функциях.
MSDN рекомендует вызывать функцию timeBeginPeriod(tc.wPeriodMin) непос редственно перед тем, как обратиться к сервису мультимедийного таймера. По вышенное разрешение таймера, по видимому, реализуется в системе при помощи создания отдельного потока с высоким приоритетом выполнения. Поэтому реко мендуется отменять режим повышенного разрешения таймера (timeEndPeriod), как только он перестает быть нужным.
1При работе с Visual Studio 6.нужно выполнить команду меню Project Settings…, перейти на вкладку Link и в текстовом поле Object/library modules указать имя библиотеки winmm.lib.
510 |
Глава 10. Таймеры и время |
|
|
Ранее мы уже использовали функции timeBeginPeriod и timeEndPeriod для повы шения точности работы функции Sleep. Степень влияния этих функций на работу собственно мультимедийного таймера мы выясним, когда проведем эксперимен ты с тестовой программой.
Функции timeSetEvent и timeKillEvent
Мультимедийный таймер выполняется в своем собственном потоке. Он отслежи вает заданный временной интервал и активизирует по истечении этого интервала таймерное событие. После этого Windows либо вызывает заданную функцию об ратного вызова, либо устанавливает заданное событие.
Сервис мультимедийного таймера вызывается с помощью функции timeSet Event:
MMRESULT timeSetEvent(UINT uDelay, UINT uResolution,LPTIMECALLBACK lpTimeProc, DWORD dwUser, UINT fuEvent);
Впараметре uDelay указывается задержка активизации таймерного события.
Вкачестве единиц измерения используются миллисекунды. Если значение пара метра выходит за пределы задержек, поддерживаемых таймером, то функция вер нет код ошибки.
Параметр uResolution содержит разрешение таймерного события в миллисекун дах. Чем меньше значение этого параметра, тем выше разрешение. Нулевое зна чение параметра означает, что периодические события таймера должны появляться с максимально возможной точностью. Однако чем выше разрешение, тем больше будут накладные расходы операционной системы, поэтому желательно выбирать максимальное значение параметра uResolution, при котором удовлетворяются по требности приложения.
Параметр lpTimeProc содержит адрес функции обратного вызова, которая вы зывается после установки таймерного события. Если параметр fuEvent содер жит флаг TIME_CALLBACK_EVENT_SET или TIME_CALLBACK_EVENT_PULSE, то пара метр lpTimeProc интерпретируется как дескриптор объекта «событие».
Параметр dwUser может содержать данные, передаваемые в функцию lpTimeProc, интерпретация которых определяется программистом. В частности, эти данные иногда используются для организации обратной связи с вызывающей функцией.
И наконец, в параметре fuEvent задается тип таймерного события. Этот пара метр может содержать один из следующих флагов:
Значение |
Интерпретация |
|
|
TIME_ONESHOT |
Событие происходит один раз после истечения uDelay миллисекунд |
TIME_PERIODIC |
Событие происходит каждые uDelay миллисекунд |
|
|
Также параметр fuEvent может содержать один из флагов, определяющих дей ствия Windows по истечении заданного интервала времени:
Значение |
Интерпретация |
|
|
TIME_CALLBACK_FUNCTION |
Вызывается функция с адресом lpTimeProc (значение по |
|
умолчанию) |
TIME_CALLBACK_EVENT_SET |
Вызывается функция SetEvent для установки события с де- |
|
скриптором lpTimeProc |
TIME_CALLBACK_EVENT_PULSE |
Вызывается функция PulseEvent для установки (с последующим |
|
сбросом) события с дескриптором lpTimeProc |
|
|
