таймеры Timers
.NET предоставляет два таймера, которые можно использовать в многопоточной среде: .NET provides two timers to use in a multithreaded environment:
- System.Threading.Timer, который выполняет метод одного обратного вызова в потоке ThreadPool с регулярными интервалами. System.Threading.Timer, which executes a single callback method on a ThreadPool thread at regular intervals.
- System.Timers.Timer, который по умолчанию порождает событие в потоке ThreadPool с регулярными интервалами. System.Timers.Timer, which by default raises an event on a ThreadPool thread at regular intervals.
В некоторых реализациях .NET может содержать дополнительные таймеры: Some .NET implementations may include additional timers:
- System.Windows.Forms.Timer — компонент Windows Forms, который вызывает событие через определенные интервалы времени. System.Windows.Forms.Timer: a Windows Forms component that fires an event at regular intervals. У этого компонента нет интерфейса пользователя. Он предназначен для однопоточной среды. The component has no user interface and is designed for use in a single-threaded environment.
- System.Web.UI.Timer — компонент ASP.NET, который выполняет асинхронную или синхронную обратную передачу веб-страницы с регулярными интервалами. System.Web.UI.Timer: an ASP.NET component that performs asynchronous or synchronous web page postbacks at a regular interval.
- System.Windows.Threading.DispatcherTimer — таймер, интегрированный в очередь Dispatcher, которая обрабатывается с заданными интервалом и приоритетом. System.Windows.Threading.DispatcherTimer: a timer that is integrated into the Dispatcher queue which is processed at a specified interval of time and at a specified priority.
Класс System.Threading.Timer The System.Threading.Timer class
Класс System.Threading.Timer позволяет непрерывно вызывать делегат через определенные интервалы времени. The System.Threading.Timer class enables you to continuously call a delegate at specified time intervals. Этот класс также можно использовать, чтобы запланировать один вызов делегата через указанный интервал времени. You can also use this class to schedule a single call to a delegate in a specified time interval. Делегат выполняется в потоке ThreadPool. The delegate is executed on a ThreadPool thread.
При создании объекта System.Threading.Timer вы указываете делегат TimerCallback, который определяет метод обратного вызова, необязательный объект состояния, который передается обратному вызову, временную задержку до первого вызова обратного вызова и интервал времени между вызовами обратного вызова. When you create a System.Threading.Timer object, you specify a TimerCallback delegate that defines the callback method, an optional state object that is passed to the callback, the amount of time to delay before the first invocation of the callback, and the time interval between callback invocations. Чтобы отменить ожидающий таймер, вызовите метод Timer.Dispose. To cancel a pending timer, call the Timer.Dispose method.
В следующем примере создается таймер, который вызывает предоставленный делегат в первый раз через одну секунду (1000 миллисекунд), а затем каждые две секунды. The following example creates a timer that calls the provided delegate for the first time after one second (1000 milliseconds) and then calls it every two seconds. Объект состояния в примере используется для подсчета вызовов делегата. The state object in the example is used to count how many times the delegate is called. Таймер останавливается после 10 вызовов. The timer is stopped when the delegate has been called at least 10 times.
System threading timer system windows forms timer
Таймеры часто играют важную роль как в клиентских приложениях, так и в компонентах программ, основанных на серверах (включая службы Windows). Написание эффективного, управляемого кода с использованием измерения реального времени требует ясного представления процесса выполнения программы и глубокого знания тонкостей многопоточной модели .NET-библиотеки. Библиотека классов .NET (.NET Framework Class Library, или .NET FCL) предоставляет 3 различные класса таймеров: System.Windows.Forms.Timer , System.Timers.Timer и System.Threading.Timer . Каждый из этих классов разработан и оптимизирован для использования в разных ситуациях. Здесь рассмотрены эти 3 класса таймеров (перевод статьи [1], автор Alex Calvo, 2004 год), что поможет Вам понять, как и когда использовать соответствующие классы таймеров.
Объекты Timer в Microsoft Windows ® позволяют Вам управлять, когда произойдет какое-либо действие в программе. Некоторые наиболее часто варианты применения таймеров: запустить процесс в запланированное время, установить интервалы между событиями, добиться нужной скорости анимации графики в интерфейсе пользователя (независимо от скорости работы процессора). В прошлом разработчики, работающие в Visual Basic ® , даже использовали таймеры для симуляции многозадачности.
Вы должны были бы ожидать, что Microsoft .NET снабдит Вас нужными инструментами для реализации различных сценариев, связанных с отслеживанием реального времени. Как уже упоминалось, в .NET Framework Class Library для этого имеется 3 разных класса таймеров: System.Windows.Forms.Timer, System.Timers.Timer и System.Threading.Timer. Первые 2 класса доступны в окне тулбокса Visual Studio® .NET, что позволяет Вам бросить эти классы на разрабатываемую форму приложения и настроить их параметры — точно так же, как делается с любым визуальным компонентом GUI интерфейса. Если будете неосторожны, то уже в этом месте могут начаться проблемы.
Тулбокс Visual Studio .NET имеет компонент управления таймером и на закладке Windows Forms, и на закладке Components (см. рис. 1). Очень просто перепутать и использовать не тот компонент, что нужно, и еще хуже — не понять при этом, чем же отличаются разные компоненты. Использовать элемент управления timer control, который находится на закладке Windows Forms, следует только если он предназначен для редактора Windows Forms. Этот элемент управления соответствует созданию экземпляра класса System.Windows.Forms.Timer для Вашей формы (окно программы). Как и все другие элементы управления в тулбоксе, Вы можете либо позволить среде Visual Studio .NET обработать инициализацию класса, либо можете инициализировать экземпляр класса таймера вручную, в Вашем коде.
Рис. 1. Различные виды элементов управления таймеров (Timer Control).
Timer control, который находится на закладке Components, можно безопасно использовать в любом классе. Этот control создает экземпляр класса System.Timers.Timer. Если Вы используете тулбокс Visual Studio .NET, то можете безопасно использовать этот таймер либо с редактором форм для окон (Windows Forms designer), либо с редактором компонента класса (component class designer). Редактор компонента класса используется в Visual Studio .NET, когда Вы работаете с классом, который является производным классом от класса System.ComponentModel.Component (как в случае, когда Вы работаете со службами Windows). Класс System.Threading.Timer не виден в окне тулбокса Visual Studio .NET. Использование этого таймера несколько более сложное, однако предоставляет большую свободу, что Вы увидите дальше в этой статье.
Рис. 2. Пример приложения, использующего разные классы таймеров.
Давайте сначала рассмотрим классы System.Windows.Forms.Timer и System.Timers.Timer. У этих двух классов очень похожая объектная модель. Далее мы рассмотрим более продвинутый класс System.Threading.Timer. На рис. 2 показан скриншот демонстрационного приложения, на которое будут ссылки в этой статье. Приложение поможет Вам получить ясное представление о каждом из классов таймеров. Вы можете загрузить исходный код приложения по ссылке [2], и поэкспериментировать с ним.
[System.Windows.Forms.Timer]
Если Вы хотели бы получить метроном, то это не то, что нужно. События таймера, генерируемые этим классом, синхронны с остальным кодом Вашего приложения Windows Forms. Это означает, что код приложения, который выполняется, никогда не будет вытесняться экземпляром этого класса таймера (если при этом предположить, что Вы не вызывали в коде приложения Application.DoEvents). Точно так же, как и остальная часть кода обычного приложения Windows Forms, любой код, который находится в обработчике событий таймера (для этого типа класса таймера) выполняется с использованием потока интерфейса пользователя приложения (UI thread). Во время ожидания UI thread также отвечает за обработку всех сообщений в очереди сообщений Windows (Windows message queue). Это включает как сообщения Windows API, так и события тиков (Tick events), генерируемые этим классом таймера. Поток UI обработает эти сообщения каждый раз, когда программа приложения не занята чем-то еще (прим. переводчика: кроме того, на обработку событий таймера также может влиять поведение и других программ, особенно если они выполняются с более высоким приоритетом).
Если Вы писали раньше программы Visual Basic до появления Visual Studio .NET, то возможно знаете, что есть только один способ своевременно позволить потоку UI отвечать на события Windows, когда выполняется какой-либо код: вызывать в этом коде метод Application.DoEvents. Подобно Visual Basic, вызов Application.DoEvents из .NET Framework может привести к многим проблемам. Application.DoEvents уступает управление обработчику сообщений UI message pump, что позволяет обработать очередь ожидающих сообщений. Это может поменять ожидаемую последовательность выполнения кода. Если Application.DoEvents вызывается из Вашего кода, то поток выполнения программы может быть прерван, чтобы обработать события таймера, генерируемые экземпляром этого класса. Это может привести к неожиданному поведению программы, трудно поддающемуся отладке.
То, как этот класс таймера ведет себя, станет очевидно при запуске демонстрационного приложения [2]. Кликните на кнопку Start, затем на кнопку Sleep, и затем на кнопку Stop, и получите следующий вывод сообщений:
В этом демонстрационном приложении свойству Interval класса таймера System.Windows.Forms.Timer присвоено значение 1000 миллисекунд. Как Вы можете увидеть, если бы обработчик события таймера продолжал получать события таймера, в то время как главный поток UI спал (время сна установлено на 5 секунд), то мы увидели бы 5 событий таймера, выведенных в окно сообщения, по одному на каждую секунду. Но так не произошло — вместо этого таймер оставался в приостановленном состоянии, когда поток UI спал.
Применение класса System.Windows.Forms.Timer не могло бы быть проще — у него очень простой и интуитивно понятный интерфейс программирования. Методы Start и Stop в действительности предоставляют альтернативный способ установки свойства Enabled (которое само по себе является тонкой оберткой вокруг функций SetTimer / KillTimer интерфейса программирования Win32 ® ). Свойство Interval, которое упоминалось ранее, своим именем говорит само за себя — оно устанавливает интервал срабатывания таймера в миллисекундах. Следует помнить, что даже если Вы можете установить свойство Interval в 1 миллисекунду, в соответствии с документацией на .NET Framework точность таймера не будет выше 55 миллисекунд (это время предоставлено потоку UI для обработки).
Захват событий, генерируемых экземпляром класса System.Windows.Forms.Timer, обрабатывается направлением события Tick на стандартный делегат EventHandler, как показано в следующем примере кода:
[System.Timers.Timer]
Документация .NET Framework описывает класс System.Timers.Timer как класс, разработанный и оптимизированный для использования в многопоточных рабочих окружениях (для применений в программах служб и серверов). Экземпляры этого класса таймера можно безопасно использовать из нескольких потоков. В отличие от System.Windows.Forms.Timer, класс System.Timers.Timer по умолчанию будет вызывать событие Вашего таймера на рабочем потоке (worker thread), полученном из пула потоков общеязыковой среды выполнения (common language runtime thread pool, пул потоков CLR). Это означает, что код внутри обработчика события Elapsed должен удовлетворять золотому правилу программирования Win32 (это правило часто доставляет головную боль неопытным разработчикам): к экземпляру любого элемента управления может получить только тот поток, который создал этот экземпляр элемента управления.
Класс System.Timers.Timer предоставляет простой путь решения этой дилеммы — он публикует свойство SynchronizingObject. Установка этого свойства в экземпляр Windows Form (или элемента управления Windows Form) гарантирует, что код в обработчике события Elapsed запустится в том же потоке, в котором был инициирован SynchronizingObject.
Если Вы используете тулбокс Visual Studio .NET, то среда Visual Studio .NET автоматически установит свойство SynchronizingObject в значение текущего экземпляра формы. Сначала может показаться, что использование этого класса таймера со свойством SynchronizingObject делает его функционально эквивалентным использованию класс System.Windows.Forms.Timer. Чаще всего так и есть. Когда операционная система оповещает класс System.Timers.Timer, что разрешенный таймер истек, таймер использует метод SynchronizingObject.Begin.Invoke для выполнения делегата события Elapsed на потоке, в котором создавался нижележащий дескриптор (handle) объекта SynchronizingObject. Этот обработчик события будет блокирован, пока поток UI не будет в состоянии обработать его. Однако, в отличие от System.Windows.Forms.Timer, событие все равно будет сгенерировано. Как Вы могли бы увидеть в листинге 1 ранее, System.Windows.Forms.Timer не может генерировать события, когда UI не может обработать их, в то время как System.Timers.Timer поставит события в очередь обработки, когда поток UI будет доступен.
В листинге 3 показан пример, как использовать свойство SynchronizingObject. Вы можете использовать демонстрационное приложение, чтобы проанализировать поведение класса System.Timers.Timer. Для этого переведите радиокнопку в во вторую позицию, и выполните у же последовательность действий, которая была проделана ранее в обсуждении тестирования класса System.Windows.Forms.Timer. Вы увидите приблизительно такие сообщения:
Как Вы можете видеть, здесь не были пропущены события таймера даже тогда, когда поток UI был в состоянии сна. Обработчик события Elapsed вызывался на каждом интервале. Несмотря на то, что поток UI спал, демонстрационное приложение вывело информацию о 5 произошедших событиях таймера (4 .. 8), во всех этих случаях поток UI просыпается, и снова может обработать очередь сообщений.
Как упоминалось ранее, члены класса System.Timers.Timer очень похожи на члены класса System.Windows.Forms.Timer. Самое большое отличие в том, что System.Timers.Timer это обертка над объектом ожидания таймера Win32, который генерирует событие Elapsed в контексте worker thread вместо генерации события Tick в контексте UI thread. Событие Elapsed должно быть соединено с обработчиком события, который соответствует делегату ElapsedEventHandler. Этот обработчик события принимает аргумент типа ElapsedEventArgs.
Кроме полей стандартного EventArgs, класс аргументов ElapsedEventArgs предоставляет public-свойство SignalTime, которое содержит точное истекшее время таймера. Поскольку этот класс поддерживает доступ из различных потоков, то можно вызвать метод Stop из другого потока, отличающегося от потока, который применяется для обработки события Elapsed. Потенциально это может привести к тому, что срабатывание события Elapsed произойдет даже после того, как был вызван метод Stop. Вы можете обработать эту ситуацию путем сравнения свойства SignalTime с временем, когда был вызван метод Stop.
Класс System.Timers.Timer также предоставляет свойство AutoReset, которое определяет должно ли событие Elapsed срабатывать с повторениями, или только 1 раз. Имейте в виду, что сброс свойства Interval после того, как таймер был запущен, приведет к тому, что текущий счетчик времени вернется обратно к нулевому значению. Например, если Вы установили интервал на 5, после чего прошло 3 секунды, и затем интервал был изменен на 10 секунд, то следующее событие таймера произойдет через 13 секунд, считая от последнего события таймера.
[System.Threading.Timer]
Третий класс таймера происходит из пространства имен System.Threading. Хотелось бы сказать: System.Threading.Timer самый лучший из всех классов таймеров, но это может ввести в заблуждение. С одной стороны, я был удивлен, что экземпляры этого класса по сути не ориентированы безопасное использование в многопоточном окружении, если учесть, что класс System.Threading.Timer находится в System.Threading namespace (очевидно, что это не означает, что класс System.Threading.Timer не может безопасно использоваться в многопоточном окружении). Интерфейс программирования этого класса не такой непротиворечивый, как у двух предыдущих рассмотренных классов таймеров, и несколько более громоздкий.
В отличие от двух предыдущих классов, класс System.Threading.Timer имеет 4 перегружаемых конструктора. Ниже показано, что это значит:
Первый параметр (callback, функция обратного вызова) требует делегата TimerCallback, который указывает на метод со следующей сигнатурой:
Второй параметр (state) может быть либо null, либо объектом, который содержит информацию, зависящую от приложения. Этот объект state передается в callback-функцию Вашего таймера всякий раз, когда возникает событие таймера. Имейте в виду, что callback-функция запускается в контексте рабочего потока (worker thread), так что Вы можете гарантировать, что имеется потокобезопасный способ доступа к объекту state.
Третий параметр (dueTime) позволяет указать, когда должно быть запущено начальное событие таймера. Вы можете указать 0, чтобы запустить таймер немедленно, или предотвратить таймер от автоматического запуска, если укажете здесь значение константы System.Threading.Timeout.Infinite.
Четвертый параметр (period) задает интервал (в миллисекундах), через который должна быть вызвана callback-функция. Если указать 0 или Timeout.Infinite, то это запретит последующие запуски событий таймера.
Как только конструктор был вызван, Вы все еще можете изменить настройки dueTime и period использованием метода Change. У этого метода также имеется четыре перезагрузки:
Ниже показан пример кода для запуска и остановки этого таймера (подобный код используется в демонстрационном приложении):
Как Вы могли бы ожидать, запуск демонстрационного приложения для класса System.Threading.Timer даст тот же вывод, который мы видели с классом System.Timers.Timer. Из-за того, что функция TimerCallback вызывается в контексте worker thread, нет пропущенных срабатываний таймера (подразумевается, что рабочие потоки могут запуститься). Листинг 5 показывает вывод приложения при тестировании System.Threading.Timer:
В отличие от класса System.Timers.Timer, здесь нет аналога свойства SynchronizingObject, который был предоставлен классом System.Timers.Timer. Любые операции, которые потребуют доступа к элементам управления пользовательским интерфейсом (UI controls), должны быть корректно маршалированы с использованием методов Invoke или BeginInvoke элементов управления.
[Потокобезопасное программирование с использованием таймеров]
Для максимального повторного использования кода демонстрационное приложение вызывает один и тот же метод ShowTimerEventFired из всех трех разных типов событий таймера. Вот эти 3 обработчика события:
Как Вы можете видеть, метод ShowTimerEventFired берет текущее время и имя текущего потока в качестве своих аргументов. Чтобы отличить рабочие потоки (worker threads) от потока UI, главная точка входа приложения устанавливает свойство Name объекта CurrentThread в «UIThread». Метод-помощник GetThreadName возвратит либо значение Thread.CurrentThread.Name, либо «WorkerThread», если свойство Thread.CurrentThread.IsThreadPoolThread равно true.
Из-за того, что события таймера для System.Timers.Timer и System.Threading.Timer выполняются в контексте рабочих потоков (worker threads), при этом обязательно, чтобы любой код интерфейса пользователя в этих обработчиках маршалировался обратно в поток UI для обработки. Чтобы сделать это, автор создал делегата, которого назвал ShowTimerEventFiredDelegate:
ShowTimerEventFiredDelegate позволяет методу ShowTimerEventFired вызвать самого себя обратно в поток UI. Листинг 6 показывает код, который все это делает.
Очень просто определить, можете ли Вы безопасно получить доступ к элементу управления Windows Forms из текущего потока, путем опроса его свойства InvokeRequired. В этом примере если у ListBox-а свойство InvokeRequired==true, то можно использовать метод BeginInvoke формы для вызова метода ShowTimerEventFired снова через делегата ShowTimerEventFiredDelegate. Это гарантирует, что метод Add элемента управления ListBox выполнится в потоке UI.
Как можете видеть, здесь есть много проблем, которых Вам нужно избегать, когда программируете асинхронные события таймера. Автор рекомендует (перед использованием либо System.Timers.Timer, либо System.Threading.Timer) к прочтению статью Ian Griffith «Windows Forms: Give Your .NET-based Application a Fast and Responsive UI with Multiple Threads» [3].
[Обработка реентрантности события таймера]
Здесь имеется другая тонкая проблема, которую Вам следует иметь в виду, когда работаете с асинхронными событиями таймера, генерируемыми такими классами, как System.Timers.Timer и System.Threading.Timer. Проблема заключается в реентрантности кода (вложенный запуск подпрограмм, reentrancy). Если код Вашего обработчика события таймера занимает для своего выполнения больше времени, чем интервал, с которым таймер генерирует события, и Вы не предприняли необходимые меры предосторожности защиты многопоточного доступа к Вашим объектам, то тогда можете столкнуться с весьма сложными проблемами в отладке. Взгляните на следующий фрагмент кода:
Предположим, что свойство Interval таймера было установлено на 1000 миллисекунд, и Вы возможно будете удивлены, что первый всплывший message box покажет значение 5. Это произошло потому, что во время 5 секунд, когда событие первого таймера спало, таймер продолжал генерировать события Elapsed на других рабочих потоках (worker threads). Таким образом, значение переменной tickCounter было инкрементировано 5 раз до того, как было завершена обработка первого события таймера. Обратите внимание, как автор использовал метод Interlocked.Increment для инкремента переменной tickCounter способом, безопасным для следы многопоточного выполнения. Есть и другие способы сделать то же самое, но метод Interlocked.Increment был специально разработан для такого рода операций.
Один из простых способов разрешить проблему реентантности такого типа — обернуть обработчик прерывания таймера в блок кода, который временно запрещает и затем разрешает таймер, как показано в следующем примере:
В этом примере кода message box будет появляться каждые 5 секунд, и как Вы можете ожидать, значение tickCounter будет инкрементироваться один раз на одно появление окна message box. Другая возможность — использование примитива синхронизации, такого как Monitor или mutex, чтобы гарантировать, что все будущие события, будут поставлены в очередь, пока текущий обработчик не завершил свое выполнение.
[Заключение]
Чтобы получить быстрый обзор на 3 класса таймеров, доступных в .NET Framework и описанных в этой статье, и сравнить их, посмотрите таблицу ниже. При решении вопроса использования таймера задумайтесь над тем, может ли Ваша проблема быть решена с помощью Планировщика Windows (Windows Scheduler) или команды AT (которая делает то же самое), что дает возможность периодического запуска стандартного выполняемого файла.
Таблица 1. Классы таймеров в .NET FCL.
Вопросы использования таймеров | System.Windows.Forms | System.Timers | System.Threading |
В контексте какого потока запускаются события таймера? | поток UI (окно формы) | поток UI или Worker thread | Worker thread |
Экземпляры класса таймера потокобезопасны? | нет | да | нет |
Понятная/интуитивная объектная модель? | да | да | нет |
Требуется наличие форм (Windows Forms)? | да | нет | нет |
Качество срабатывания тиков как у метронома? | нет | да* | да* |
Событие таймера поддерживает объект state? | нет | нет | да |
Может ли быть запланирован запуск первого события таймера? | нет | нет | да |
Поддерживает ли класс наследование (inheritance)? | да | да | нет |
Примечание *: в зависимости от доступности системных ресурсов (например, worker threads).