Rambler's Top100
"Knowledge itself is power"
F.Bacon
Поиск | Карта сайта | Помощь | О проекте | ТТХ  
 Подземелье Магов
  
 

Фильтр по датам

 
 К н и г и
 
Книжная полка
 
 
Библиотека
 
  
  
 


Поиск
 
Поиск по КС
Поиск в статьях
Яndex© + Google©
Поиск книг

 
  
Тематический каталог
Все манускрипты

 
  
Карта VCL
ОШИБКИ
Сообщения системы

 
Форумы
 
Круглый стол
Новые вопросы

 
  
Базарная площадь
Городская площадь

 
   
С Л С

 
Летопись
 
Королевские Хроники
Рыцарский Зал
Глас народа!

 
  
ТТХ
Конкурсы
Королевская клюква

 
Разделы
 
Hello, World!
Лицей

Квинтана

 
  
Сокровищница
Подземелье Магов
Подводные камни
Свитки

 
  
Школа ОБЕРОНА

 
  
Арсенальная башня
Фолианты
Полигон

 
  
Книга Песка
Дальние земли

 
  
АРХИВЫ

 
 

Сейчас на сайте присутствуют:
 
  
 
Во Флориде и в Королевстве сейчас  23:39[Войти] | [Зарегистрироваться]

Таймер, который не подведет

Александр Малыгин
дата публикации 18-07-2001 13:51

Таймер, который не подведет

Мысль о хорошем таймере давно волнует умы программистов. Сразу оговорюсь, что речь не идет о прецизионном, "высокочастотном" иструменте отсчета интервалов времени, с дискретностью 1 мс и менее, как иногда хочется. Для этого существуют иные методы и/или иные операционные системы.

Здесь же будет построен просто надежный таймер общего назначения, который "тикнет" вовремя, во что бы то ни стало. Реализация в пределах стандартных возможностей Win32API, т.е. ничего "военного". Плюс одна интересная идея, заимствованная из мира Unix.

Немного теории
Стандартные средства

Как известно, компонент TTimer основан на User таймерах, предоставляемых Win32API. Это просто VCL-оболочка, удобная для использования "на форме" системных таймеров данного типа. Для их работы требуется окно, поскольку программа получает извещение о срабатывании таймера посредством сообщения WM_TIMER. Без участия VCL для работы с ними необходим обработчик сообщения WM_TIMER и функции API:

CreateTimer
SetTimer
KillTimer

Да чем же они плохи, эти User таймеры? А вот чем!

  • Недостаток 1.

Как известно, сообщения в Win32 обрабатываются в следующем порядке очередности:

  1. синхронные сообщения (от SendMessage)
  2. асинхронные сообщения (от PostMessage)
  3. завершение приложения или WM_QUIT (от PostQuitMessage)
  4. асинхронный ввод (мышь, клавиатура)
  5. требование прорисовки или WM_PAINT
  6. извещения от User таймеров или WM_TIMER

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

  • Недостаток 2.

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

  • Недостаток 3.

Нельзя сделать так, чтобы моменты срабатывания были привязаны к системному времени (были синхронны с ним), т.е. чтобы таймер тикал в определенные часы, минуты, секунды. Он обязательно будет "уплывать".

  • Недостаток 4.

О срабатывании таймера User уведомляется только один поток (тот, который вызвал SetTimer), поэтому невозможно пробудить по таймеру сразу несколько потоков.

  • Недостаток 5.

Дискретность временного интервала оставляет желать лучшего. В 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 в современном виде.

Тактико-технические характеристики

Подсистема виртуальных таймеров (или Таймерный менеджер). Предоставляет любое количество программных объектов для отсчета времени, независимых от загрузки системы приложениями и работой пользователя, следующих типов:

  1. интервальный таймер (одновибратор/мультивибратор);
    • точность — 10 миллисекунд;
    • управление: пуск, останов, задание периода и режима.
  2. синхронизированный таймер (будильник), привязан к системному времени;
    • набор моментов срабатывания конфигурируется строкой в формате 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 такова:

Секунды Минуты Часы Дни Месяцы ДниНедели

Если какая-либо единица времени/даты имеет произвольное значение, то ее просто опускают (если все старшие единицы тоже произвольны) или список ее значений представляют знаком "*" (если соседняя старшая единица задана).

Примеры записи периодических событий в формате 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
    Нормальные условия
    10010000100991003-12
    100100000100099810003-12
    10100001000659999100
    101000001000063619991100
    151000066748266744-60
    Нагрузка приложения (задержка на 2000 мс)
    10010000100791002-17
    15100006673336672-100
    101000010001569992-100
    Внешняя нагрузка (играющий Winamp)
    100100001009710028-51
    1510000667175632100
    1010000100022894100

    Александр Малыгин
    Специально для Королевства Delphi

    Исходные тексты программ, приложенные к данной статье, распространяются на правах freeware.


    К материалу прилагаются файлы:




    Смотрите также материалы по темам:
    [TMemo] [TTimer] [Таймеры]

     Обсуждение материала [ 20-10-2017 09:07 ] 51 сообщение
      
    Время на сайте: GMT минус 5 часов

    Если вы заметили орфографическую ошибку на этой странице, просто выделите ошибку мышью и нажмите Ctrl+Enter.
    Функция может не работать в некоторых версиях броузеров.

    Web hosting for this web site provided by DotNetPark (ASP.NET, SharePoint, MS SQL hosting)  
    Software for IIS, Hyper-V, MS SQL. Tools for Windows server administrators. Server migration utilities  

     
    © При использовании любых материалов «Королевства Delphi» необходимо указывать источник информации. Перепечатка авторских статей возможна только при согласии всех авторов и администрации сайта.
    Все используемые на сайте торговые марки являются собственностью их производителей.

    Яндекс цитирования