Александр Малыгин дата публикации 18-07-2001 13:51 Таймер, который не подведет
Мысль о хорошем таймере давно волнует умы программистов. Сразу оговорюсь,
что речь не идет о прецизионном, "высокочастотном" иструменте отсчета интервалов
времени, с дискретностью 1 мс и менее, как иногда хочется. Для этого существуют
иные методы и/или иные операционные системы.
Здесь же будет построен просто надежный таймер общего назначения, который "тикнет"
вовремя, во что бы то ни стало. Реализация в пределах стандартных возможностей
Win32API, т.е. ничего "военного". Плюс одна интересная идея, заимствованная из
мира Unix.
Стандартные средства
Как известно, компонент TTimer основан на User таймерах, предоставляемых
Win32API. Это просто VCL-оболочка, удобная для использования "на форме" системных
таймеров данного типа. Для их работы требуется окно, поскольку программа получает
извещение о срабатывании таймера посредством сообщения WM_TIMER. Без участия VCL
для работы с ними необходим обработчик сообщения WM_TIMER и функции API:
CreateTimer
SetTimer
KillTimer
Да чем же они плохи, эти User таймеры? А вот чем!
Как известно, сообщения в Win32 обрабатываются в следующем порядке очередности:
- синхронные сообщения (от SendMessage)
- асинхронные сообщения (от PostMessage)
- завершение приложения или WM_QUIT (от PostQuitMessage)
- асинхронный ввод (мышь, клавиатура)
- требование прорисовки или WM_PAINT
- извещения от User таймеров или WM_TIMER
Таким образом, сообщение WM_TIMER имеет наименьший приоритет и принимается только
когда в очереди потока нет других сообщений. Это значит, что своевременному
получению "тика" от User таймера может помешать как длительная обработка синхронных
и асинхронных сообщений, так и реакция на интенсивный ввод пользователя, и даже
активная перерисовка окон.
Кроме того, повторные сообщения WM_TIMER от того же таймера уничтожаются,
если в очереди еще есть необработанное такое же. Все это приводит к потере
тиков таймера, в результате программа получает меньше вызовов обработчика
таймера на заданном интервале, чем рассчитывал программист.
Нельзя сделать так, чтобы моменты срабатывания были привязаны к системному времени
(были синхронны с ним), т.е. чтобы таймер тикал в определенные часы, минуты, секунды.
Он обязательно будет "уплывать".
О срабатывании таймера User уведомляется только один поток (тот, который вызвал
SetTimer), поэтому невозможно пробудить по таймеру сразу несколько потоков.
Дискретность временного интервала оставляет желать лучшего. В Win9x она составляет
55 мс. Но даже в NT интервал квантуется не менее чем по 10 мс.
Альтернатива
Есть, конечно, альтернатива User таймерам — это ожидаемые таймера, реализованные
в ядре и поэтому менее тяжеловесные и более надежные. Они не посылают сообщений
и должны ожидаться с помощью функции WaitForSingleObject или подобной. К ним
имеют прямое отношение следующие функции API:
CreateWaitableTimer
SetWaitableTimer
CancelWaitableTimer
Но, к сожалению, эти функции реализованы только в Windows NT/2000 и, следовательно
не подходят для программы, рассчитанной на любую платформу Win32.
Что же, все-таки, можно сделать
Есть три базовых идеи, три "кита", на основе которых можно построить надежный
таймерный сервер:
- Функция Sleep, которая позволяет отсчитать заданное количество миллисекунд,
не загружая при этом процессор.
- Отдельный поток, в котором будет крутиться цикл для отслеживания времени
срабатывания.
- Набор средств уведомления клиентов (в других потоках приложения) о тиках
таймеров — объекты ядра для синхронизации потоков, оконные сообщения и др.
Функция Sleep имеет дискретность отработки заданного интервала
(по результатам эксперимента) 10 мс в Windows NT и примерно 3-4 мс в Windows 98.
Во многих случаях достаточно просто вызвать эту функцию там, где нужна задержка,
если потоку больше нечем заняться в течение этого интервала времени.
Цикл в отдельном потоке с вызовом Sleep с постоянным интервалом
и опросом списка таймерных объектов (у каждого свой интервал, заданный клиентом)
с определением момента срабатывания, позволяет обрабатывать столько виртуальных
таймеров, сколько потребуется программе. Высокий приоритет, заданный потоку,
даст возможность таймерам "тикать" даже тогда, когда другие потоки заняты работой.
Использование библиотеки классов, инкапсулирующих системные средства межпоточного
взаимодействия (см. статью "Набор объектов-нотификаторов")
позволит клиентам выбирать наиболее подходящий для конкретного случая способ извещения.
Упоминаемые термины виртуальный таймер, таймерный менеджер
имеют для данной разработки историческое происхождение.
В начале 90-х годов прошлого века я занимался разработкой контроллеров на i8051
и софта для них (макроассемблер 2500 A.D.). И был тогда сделан "драйвер
виртуальных таймеров", расширяющий возможности однокристалки (у нее всего два
аппаратных таймера) по обеспечению программы инструментами отсчета времени.
Будильников там еще не было. Работа велась в обработчике аппаратного прерывания.
В 1993 году в составе программы верхнего уровня системы учета энергоресурсов в среде DOS
(Turbo-Pascal), в разработке которой я участвовал, был таймерный менеджер
(тот самый TIMERMAN). Он предоставлял набор интервальных таймеров и ежесуточных
будильников, имея обработчики прерываний стандартного таймера ($1C) и
будильника RTC ($4A). Интервал в секундах до 65535. Обработка таймеров выполнялась,
когда менеджер получал управление в общем цикле программы (была организована
кооперативная многозадачность между модулями). Клиент мог сам проверять таймер
или передать адрес своей процедуры — натуральный callback. Позднее, с переходом
на BP7 и protected mode, менеджер перекочевал в независимую DLL.
В 1997 году Timerman был портирован под OS/2 (Virtual Pascal) без изменений в
архитектуре — только прерывание было заменено на Thread.
В 1999 году в связи с разработкой системы учета под Windows CE был разработан
заново таймерный менеджер, и был он в виде DLL. Практически это было то,
что я сейчас предлагаю, только реализация на VC++ (без использования MFC).
В том же году Timerman.dll был переписан на Delphi в современном виде.
Тактико-технические характеристики |
Подсистема виртуальных таймеров (или Таймерный менеджер).
Предоставляет любое количество программных объектов для отсчета
времени, независимых от загрузки системы приложениями и работой
пользователя, следующих типов:
- интервальный таймер (одновибратор/мультивибратор);
- точность — 10 миллисекунд;
- управление: пуск, останов, задание периода и режима.
- синхронизированный таймер (будильник), привязан к системному времени;
- набор моментов срабатывания конфигурируется строкой в формате CRON,
позволяющем простым способом описывать сложные периодические события;
- дискретность настройки — от секунды до месяца;
- управление: пуск, останов, задание маски времени и режима.
Реализовано все это в виде DLL — для возможности использования не только в
программах на Delphi. Впрочем, можно использовать Subj просто как библиотеку
классов — модуль Timers.pas. При желании можно натянуть на это дело компонентную
крышу, но у меня такой необходимости не возникало. В нынешнем виде его можно
использовать в программах как с формами, так и вообще без "морды", т.к. он не
использует VCL.
Разработано и отлажено в среде Delphi 5, но будет компилироваться и в более ранних -
может понадобиться замена типа dword на что-нибудь похожее (беззнаковость здесь
роли не играет).
Все исходные тексты и откомпилированная DLL собраны в архив timerman.zip.
Тестовая программа (исходные тексты) отдельно в файле tmdemo.zip
Для интересующихся — сорцы версии на С++ в файле tmmancpp.zip.
Здесь приведен текст модуля импорта для использования Timerman.dll.
unit TmImport;
interface
uses Windows,NotifyDef;
const TimerMan = 'TimerMan.dll';
(*** Creating interval timer with object event handler ***)
function tmCreateIntervalTimer(
hEventProc: TNotifierEvent; // Client event handler
Interval : dword; // Time interval, msec
Mode : byte; // Timer mode
Run : boolean; // Start timer immediately
Msg, // Message code (2nd handler parameter)
UserParam : dword // User parameter (3rd handler parameter)
) : THandle;
external TimerMan name 'tmCreateIntervalTimer';
(*** Creating interval timer ***)
function tmCreateIntervalTimerEx(
hEventObj : THandle; // Notify object handle
Interval : dword; // Time interval, msec
Mode : byte; // Timer mode
Run : boolean; // Start timer immediately
EventType : byte; // Notify object type
Msg, // Message code
UserParam : dword // User parameter for message
) : THandle;
external TimerMan name 'tmCreateIntervalTimerEx';
(*** Closing timer ***)
procedure tmCloseTimer(hTimer : THandle);
external TimerMan name 'tmCloseTimer';
(*** Starting timer (enable work) ***)
procedure tmStartTimer(hTimer : THandle);
external TimerMan name 'tmStartTimer';
(*** Stopping timer (disable work) ***)
procedure tmStopTimer(hTimer : THandle);
external TimerMan name 'tmStopTimer';
(*** Resetting timer ***)
procedure tmResetTimer(hTimer : THandle);
external TimerMan name 'tmResetTimer';
(*** Set timer mode ***)
procedure tmSetTimerMode(hTimer : THandle; Mode : byte);
external TimerMan name 'tmSetTimerMode';
(*** Modify timer interval ***)
procedure tmSetTimerInterval(hTimer : THandle; Interval : dword);
external TimerMan name 'tmSetTimerInterval';
(*** Creating synchronized period timer with object event handler ***)
function tmCreateFixedTimer(
hEventProc: TNotifierEvent; // Client event handler
TimeMask : ShortString;// Time period in CRON format
Mode : Byte; // Timer mode
Run : Boolean; // Start timer immediately
Msg, // Message code
UserParam : dword // User parameter for message
) : THandle;
external TimerMan name 'tmCreateFixedTimer';
(*** Creating synchronized period timer ***)
function tmCreateFixedTimerEx(
hEventObj : THandle; // Notify object handle
TimeMask : ShortString;// Time period in CRON format
Mode : Byte; // Timer mode
Run : Boolean; // Start timer immediately
EventType : Byte; // Notify object type
Msg, // Message code
UserParam : dword // User parameter for message
) : THandle;
external TimerMan name 'tmCreateFixedTimerEx';
(*** Modify fixed timer CRON mask ***)
procedure tmSetTimerMask(hTimer : THandle; TimeMask : shortstring);
external TimerMan name 'tmSetTimerMask';
(*** Load fixed timer LastTime ***)
procedure tmSetLastTime(hTimer : THandle; var LastTime : TSystemTime);
external TimerMan name 'tmSetLastTime';
(*** Save fixed timer LastTime ***)
procedure tmGetLastTime(hTimer : THandle; var LastTime : TSystemTime);
external TimerMan name 'tmGetLastTime';
implementation
end.
| |
Для задания моментов срабатывания синхронизированного таймера используется формат
CRON (юниксоиды в курсе — это демон регулярно выполняемых заданий).
Идея простого способа записи в строковой форме периодических событий любой сложности,
привязанных к астрономическому времени, пришлась очень кстати.
Здесь используется модифицированный формат CRON (добавлены секунды, расширены
правила определения списков).
Строка CRON представляет собой несколько списков чисел,
разделенных пробелом. Каждый список задает перечень моментов времени или даты,
в единицах, зависящих от позиции (номера) списка в строке.
Последовательность списков в строке CRON такова:
Секунды Минуты Часы Дни Месяцы ДниНедели
Если какая-либо единица времени/даты имеет произвольное значение, то ее просто
опускают (если все старшие единицы тоже произвольны) или список ее значений
представляют знаком "*" (если соседняя старшая единица задана).
Примеры записи периодических событий в формате CRON (с вариантами):
Каждую минуту в 0 секунд и 30 секунд: |
0,30 0+30 +30 |
Каждую секунду в 0 часов, 8 и 16 часов: |
0-59 0-59 0-16+8 * * 0+8 * * +8 |
Начало каждого часа, исключая полночь и полдень: |
0 0 1-11,13-23 |
Каждые 3 секунды 1 числа каждого месяца: |
0-59+3 * * 1 +3 * * 1 |
30 минут 0 секунд каждого часа в воскресенье: |
0 30 * * * 0 |
Строка списка без пробелов представляет собой набор групп
чисел, разделенных запятой ",".
Группа может состоять из одного числа или диапазона. Последний
задается двумя числами, разделенными дефисом "-" (начальное и конечное значение).
Опционально после диапазона может стоять значение шага, отделенное знаком плюс
"+". По-умолчанию шаг равен 1. Если шаг указан, то конечное значение можно
опустить — тогда оно по-умолчанию будет равно максимальному значению в
контексте назначения данного списка. Начало диапазона по-умолчанию равно 0.
Символ звездочки "*" вместо группы означает весь диапазон возможных
значений в данном контексте. Порядок следования групп в строке списка роли не играет.
Пример. Cписок вида: 0-5,8,12,20-30+2
интерпретируется как последовательность: 0,1,2,3,4,5,8,12,20,22,24,26,28,30
Особенности реализации будильника |
Входная строка CRON для синхронизированного таймера не хранится в экземпляре
класса и не используется непосредственно для определения необходимости "тикнуть"
в основном цикле менеджера. Вместо этого сразу производится разбор строки и
преобразование во внутренний формат маски времени. Последний представляет собой
массив множеств временных единиц (множество секунд, минут и так далее). Это
позволяет выполнять операцию сравнения с текущим временем очень быстро -
практически одноактная операция, не зависящая от длины исходной строки.
Как и IntervalTimer, FixedTimer может работать в периодическом и старт-стопном
режиме (параметр Mode=tmPeriod,tmStartStop в tmCreateFixedTimer).
Но имеется еще дополнительная опция "уверенной синхронизации" (Mode=tmSureSync).
В этом режиме производится проверка на пропуск предыдущего момента срабатывания.
При этом, если даже в один прекрасный момент что-то помешало таймеру "тикнуть"
(в течение более 1 с поток таймерного менеджера не получал управление), в
следующую секунду он обязательно сработает "за тот раз". Время последнего
срабатывания запоминается, его можно прочитать и установить.
Разумеется, в мире нет ничего абсолютного. А тем более когда дело касается столь
нереалтаймовой системы как Уиндоус. Впрочем, в случае real-time OS вопрос о
таймерах вообще бы не стоял. А в наших условиях вполне может найтись в системе
какой-нибудь хулиганский поток с высоким приоритетом (выше или равным нашему),
который наглым образом будет отбирать управление на длительное время (больше
длительности внутреннего цикла таймерного менеджера — 10 мс). И тогда наш таймер
будет пропускать "тики" на коротких заданных интервалах и увеличивать погрешность
на длинных. Это происходит в том случае, если кто-то работает не по правилам,
либо производительности процессора не хватает для работы системы.
В целях проверки и демонстрации функционирования таймерного менеджера, а также
сравнительного анализа со стандартным таймером была разработана демо-программа
(tmdemo.zip).
Сравнивались: компонент TTimer, два интервальных таймера с разными способами
уведомления (сообщение окну и асинхронный вызов), один синхронизированный таймер.
Подсчитывалось количество срабатываний. Каждый факт срабатывания таймера
записывался в журнал (TMemo), что также играло роль полезной нагрузки в работе
приложения (попросту отъедание процессорного времени). Дополнительная нагрузка
по инициативе пользователя эмулировалась задержкой (sleep) в обработчике событи
OnClick кнопки. Одновременно контролировалась загрузка процессора по показаниям
Windows NT Task Manager.
Проведенные исследования показали (см. таблицу), что интервальный таймер
ведет себя почти идеально от 100 мс и достаточно хорошо на более мелких интервалах,
тогда как стандартный таймер на коротких интервалах, а особенно под нагрузкой
совсем сдает позиции. На интервале 10 мс интенсивная обработка извещений от
таймеров (обновление контролов, особенно TMemo) приводит к 100% загрузке процессора.
Синхронизированный таймер (FixedTimer), заряженный на минимальный интервал 1 с,
всегда давал точное число тиков, причем срабатывал в начале секунды с небольшим
разбросом.
От способа уведомления количество полученных тиков не зависело. При большой
нагрузке и высокой частоте приложение могло получать уведомления PostMessage
неравномерно (пачками накопленных в очереди сообщений), но общее число
выдерживалось, насколько это возможно.
Результаты приведены для следующей конфигурации: Cyrix 6x86PR233/64M/WinNT4.
Измерения проводились также на платформе Win98SE, где IntervalTimer показал
примерно те же результаты, а TTimer еще более худшие.
Интервал таймера, мс |
Интервал измерения, мс |
Количество срабатываний |
Total CPU usage, % |
Идеальный таймер |
TTimer |
IntervalTimer |
Нормальные условия |
100 | 10000 | 100 | 99 | 100 | 3-12 |
100 | 100000 | 1000 | 998 | 1000 | 3-12 |
10 | 10000 | 1000 | 659 | 999 | 100 |
10 | 100000 | 10000 | 6361 | 9991 | 100 |
15 | 10000 | 667 | 482 | 667 | 44-60 |
Нагрузка приложения (задержка на 2000 мс) |
100 | 10000 | 100 | 79 | 100 | 2-17 |
15 | 10000 | 667 | 333 | 667 | 2-100 |
10 | 10000 | 1000 | 156 | 999 | 2-100 |
Внешняя нагрузка (играющий Winamp) |
100 | 10000 | 100 | 97 | 100 | 28-51 |
15 | 10000 | 667 | 175 | 632 | 100 |
10 | 10000 | 1000 | 22 | 894 | 100 |
Александр Малыгин
Специально для Королевства Delphi
Исходные тексты программ, приложенные к данной статье, распространяются на правах
freeware.
К материалу прилагаются файлы:
[TMemo] [TTimer] [Таймеры]
Обсуждение материала [ 20-10-2017 09:07 ] 51 сообщение |