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

лекции / Shchupak_Yu._Win32_API_Razrabotka_prilozheniy_dlya_Windows

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

Программирование задержек в исполнении кода

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++;

продолжение

Стандартный таймер

507

 

 

Листинг 10.5. (продолжение)

if (count == N) InvalidateRect(hWnd, NULL, TRUE); break;

case WM_PAINT:

hDC = BeginPaint(hWnd, &ps);

sprintf(text, "Фактическая величина задержки для TIME_PERIOD = %d", TIME_PERIOD); TextOut(hDC, 0, 20, text, strlen(text));

/* код, аналогичный приведенному в листинге 10.3 */ EndPaint(hWnd, &ps);

break;

case WM_DESTROY: KillTimer(hWnd, ID_TIMER); PostQuitMessage(0);

break;

default:

return DefWindowProc(hWnd, uMsg, wParam, lParam);

}

return 0;

}

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

Результаты тестирования представлены в табл. 10.4.

Таблица 10.4. Замеры реального интервала срабатывания стандартного таймера

TIME_PERIOD,

Реальный

 

 

Ближайшее большее

ìñ

интервал, мс

 

 

значение, кратное

 

 

 

 

разрешению систем-

 

 

 

 

ного таймера, мс

 

 

 

 

 

 

Минимальный

Максимальный

Средний

 

 

 

 

 

 

1

14,79

16,16

15,62

15,625

10

15,09

16,17

15,62

15,625

20

30,47

32,07

31,26

31,25

40

46,18

47,56

46,88

46,875

100

108,90

110,10

109,38

109,375

1000

1000,07

1015,70

1000,86

1000

 

 

 

 

 

Отметим, что указанные результаты получены для работы приложения как бы в «стерильных условиях». Иными словами, во время эксперимента на компьюте ре не было других работающих ресурсоемких приложений. Это раз. Кроме того, пользователь не прикасался к окну тестируемого приложения. Это два.

Если же во время эксперимента многократно перемещать с помощью мыши окно работающего приложения, то разброс значений реального интервала сра батывания таймера1 будет совсем другим. Например, для заказанного интерва ла 100 мс был получен разброс 0,52–503,38 мс. Этот факт требует некоторых объяснений, но сначала нужно разобраться, каков механизм продвижения сооб щения WM_TIMER от виртуального таймера к оконной процедуре.

1 Разница между минимальным и максимальным значениями.

508

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

 

 

Таймерные прерывания по своей природе являются асинхронными, так как по отношению к выполняемой программе их появление предсказать нельзя. Они ста вятся в обычную очередь сообщений приложения и обрабатываются, как все остальные сообщения. Но это еще не все. Сообщение WM_TIMER обладает самым низким приоритетом по отношению к другим сообщениям. Единственным иск лючением является сообщение WM_PAINT, обладающее еще более низким приори тетом. Поэтому функция GetMessage отправляет сообщение WM_TIMER на обработ ку только тогда, когда в очереди сообщений не осталось более приоритетных сообщений.

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

Сообщения WM_TIMER похожи на сообщения WM_PAINT еще в одном аспекте. Когда приложение чем то занято и не успевает обработать предыдущее сообще ние WM_TIMER, то Windows не накапливает в очереди сообщений несколько со общений WM_TIMER. Вместо этого Windows объединяет несколько сообщений WM_TIMER из очереди в одно сообщение. В такой ситуации приложение не способ но определить количество потерянных сообщений WM_TIMER.

Теперь мы можем объяснить наблюдаемый разброс 0,52–503,38 мс для реаль ного интервала срабатывания таймера при ожидаемом интервале 109,375 мс. Зна чение 503,38 мс отражает ситуацию, когда четыре последовательных сообщения WM_TIMER были потеряны и только пятое из них было обработано. Причем оно попало в очередь сообщений не сразу, а с задержкой примерно 43,5 мс. Значение 0,52 мс могло получиться в результате аналогичной задержки примерно на 108,855 мс при постановке в очередь предыдущего сообщения WM_TIMER (из за обработки сообщения WM_MOVING с более высоким приоритетом), поэтому от те кущего интервала осталось примерно 0,52 мс.

Пример первого способа использования стандартного таймера вы можете най ти в листинге 12.1 (глава 12).

Второй способ использования стандартных таймеров

При первом способе установки таймера сообщения WM_TIMER посылаются в обычную оконную процедуру. Второй способ предписывает Windows пересы лать сообщения таймера другой функции из вашей программы. Это функция об ратного вызова (call back), и она должна быть определена в глобальной области видимости следующим образом1:

VOID CALLBACK TimerProc(HWND hwnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime) {

… // обработка сообщения WM_TIMER

}

Перед установкой таймера вы должны определить переменную для сохране ния идентификатора таймера, например:

static int idTimer;

1 Вместо имени TimeProc можно использовать любое другое имя.

Стандартный таймер

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