Timers are very useful in C# for UI programming, games, and applications have logic based on time intervals.
Timers that C# has available through .NET make it easy to time intervals and also to perform specific tasks at specific intervals.
The C# Timer class is a .NET class that helps you create an event that will happen at a set interval. The interface makes it easy to start and stop a timer or enable and disable it.
Timer Usage
The key terms when using the timer class are:
- Interval: Time interval between two successive invocations of Elapsed function.
- Callback: The timer invokes this method at the end.
- AutoReset: Boolean that determines whether the timer will raise the Tick event each time the specified interval has elapsed.
To use a timer in C#, follow these steps:
-
Setup the timer with your desired interval in milliseconds.
-
Define the
Elapsed
event handler. The timer will call this handler every time the interval milliseconds have elapsed. -
Setup the timer to call your event handler:
-
Call the Start() method to start the timer.
-
Once done with the timer, call the Dispose() method to free up the resources used by the Timer.
Putting it all together:
Plus equal event
The += (plus equal) syntax in C# is a shortcut for defining an event handler. It tells the timer to subscribe to the event handler.
We can also use -= (minus equal) to unsubscribe if needed.
Interval limitation
The C# Timer depends on the system clock.
The resolution of the system clock is how often the Elapsed event will fire. If you set the Interval property to be less than resolving the system clock, then it won’t fire at the desired interval.
For most use cases that won’t be a problem. But if you are looking for high-resolution timer, then consider using the Stopwatch class.
Disposing
Timer class implements System.IDisposable interface to release the system resources allocated by it when disposed.
Best practices when working in C# say that you should clean up your resources if the class implements IDisposable interface.
There are two ways to dispose the Timer:
- Dispose the timer by calling the Dispose() method inside a try/catch block to avoid exception.
- Use the
using
directive to automatically call the Dispose method when a particular scope is clean up. This will avoid the extra try/catch block and code lines to clean up properly.
Async timer
The System.Timers.Timer class support async event handler to fire the event at the desired time interval. To define an async event handler, use the async
modifier before your signature for Elapsed event handler.
Example:
Keep in mind that the async callback must return a Task. If the async method runs longer than the interval, you might have some unexpected results.
→ Read more: C# Async vs sync
Different timers in .NET
In .NET, there are 4 different timers depending on the way you want to use them:
- System.Windows.Forms.Timer
- The System.Web.UI.Timer
- System.Timers.Timer
- System.Threading.Timer
System.Windows.Forms.Timer
The System.Windows.Forms.Timer class is specifically designed for rich client user interfaces.
Programmers can drag it into a form as a nonvisual control and regulate the behavior from within the Properties window. It will always safely fire an event from a thread that can interact with the user interface.
System.Web.UI.Timer
The System.Web.UI.Timer can perform Web page post backs at a defined interval, either asynchronously or synchronously. It’s part of the ASP.NET framework, so we can use it only in ASP.NET Web applications.
This timer is useful for creating a real-time display of information on an ASP.NET application.
System.Threading.Timer
The System.Threading.Timer provides the ability to schedule a single callback on a background thread, either asynchronously or synchronously.
System.Threading.Timer is thread-safe.
System.Timers.Timer
System.Timers.Timer is the best way to implement a timer in C#.
System.Timers.Timer is a wrapper for System.Threading.Timer, abstracting and layering on functionality.
You cannot use System.Threading.Timer as a component within a component container, something that implements System.ComponentModel.IContainer, because it does not derive from System.ComponentModel.Component.
System.Timers.Timer extends the capabilities of the System.Timers.Timer to include features necessary for component container applications.
System.Timers.Timer and System.Threading.Timer are both designed for use in server-type processes, but System.Timers.Timer includes a synchronization object to allow it to interact with the user interface, which is helpful for applications that need to keep track of user input or updates.
When to use which timer?
Follow these guidelines to choose which one to use:
- System.Windows.Forms.Timer — For Windows Forms application, runs the delegate on a form’s UI thread.
- System.Web.UI.Timer — For ASP.NET component. It allows you to perform asynchronous or synchronous web page post backs at regular intervals.
- System.Threading.Timer — great for background tasks on a thread pool.
- System.Timers.Timer — wraps the System.Threading.Timer with the simpler API. We use this one most of the time.
C# Timer Accuracy
The C# Timer is not accurate. The timer, on average, takes 9.07% longer than it is supposed to, according to research from Lawrence Technological University.
To get accurate time, we need to use the .NET StopWatch class.
System.Diagnostics.Stopwatch
The stopwatch class is used to time code execution. It can be started, stopped/paused, and reset.
The main difference between C# Stopwatch and C# Timers is that while the Timer triggers an event when a certain interval has elapsed, Stopwatch calculates how much time has passed since it started. Moreover, the Stopwatch is much more accurate and can measure time to a greater level of precision.
If you want a high-resolution timer, consider using the Stopwatch instead of the Timer.
EventHandler vs ElapsedEventHandler
The main difference between EventHandler and ElapsedEventHandler is that EventHandler is more generic and ElapsedEventHandler works with a delegate that has a specific signature.
ElapsedEventHandler is a delegate that passes ElapsedEventArgs which extends EventHandler’s arguments by adding FileTime
and SignalTime
. The added logic makes the ElapsedEventHandler delegate compatible with ElapsedEventArgs.
FAQ
What is an event in C#?
An event in C# is a way for a class to let clients know when something interesting happens with an object. We declare events using delegates.
Terms subscribe and unsubscribe are sometimes used to show that a class is interested in a particular event and wants to know when it occurs.
We often don’t need event arguments, so we can implement that as an optional argument in the event handler.
How to name events in C#?
The naming convention suggest naming events in the past tense. For example, OrderPlaced, OrderShipped, etc.
The convention also suggest naming event handler as a combination of event name and EventHandler
suffix. For the events above, the event handler would be named OrderPlacedEventHandler
, OrderShippedEventHandler
.
Conclusion
Timers are a great way to handle time-based operations in C#. They can be used for everything from UI programming to game logic and more.
The timer class is easy to use, and makes it simple to set up an event that will happen at a specific interval.
In order to use a timer in C#, you must first setup the timer with your desired interval and then define the Elapsed event handler. Once the timer is started, it will call the event handler every time the specified interval has elapsed.
If you need a high-resolution timer, consider using the Stopwatch class. When you’re done using the timer, be sure to dispose of it properly by calling the Dispose() method.
The .NET provides a variety of timers to suit your needs, depending on how you want to use them. System.Windows.Forms.Timer is specifically designed for rich client user interfaces, and can be regulated from within the Properties window. System.Web.UI.Timer can be used in ASP.NET Web applications to create real-time displays of information, while System.Threading.Timer is great for background tasks on a thread pool.
Table of Contents
- Introduction
- What is timer?
- Type of Timer in .NET Framework
- Schedule a Task
- Basic Functions
- Code Usage
- Output
- Explanation
- Summary
- Reference
- See Also
- Download
Introduction
In this article we will see how to use Timer class in .NET Framework under System.Timers namespace. The example is made of C# use under Console Application. Often we need to schedule our task like need for an event to be triggered at various absolute time.
For example every 24 hour, everyday at certain time etc. We can achieve this in various way like using Windows Task Scheduler. But here we will see how we can use Timer class in .NET Framework to achieve this scheduled job. This sample illustrates a way to
let user to know how they can schedule a task using timer.
↑ Return to Top
What is timer?
According to MSDN
documentation Generates an event after a set interval, with an option to generate recurring events. So, the Timer allows us to set a time interval to periodically execute an event at a specified interval. It is useful when we want to execute
certain functions/applications after a certain interval.
The C# timer event keeps track of time just like a clock would, it is basically an accessible clock and counts in milliseconds, thousandths of a second. This allows for great detail.
Code structure inside the timer:
private
void
timer1_Tick(object
sender, EventArgs e)
{
//events occur when timer stops
timer.Stop();
Console.WriteLine(
"Hello World!!!"
);
//Code to Perform Task goes in between here
timer.Start();
}
↑ Return to Top
Type of Timer in .NET Framework
The .NET Framework Class Library provides following different timer classes:
- System.Windows.Forms.Timer (Windows-based timer)
- System.Timers.Timer (Server-based timer)
- System.Threading.Timer (Thread timer)
- System.Web.UI.Timer (Web-based timer)
- System.Windows.Threading.DispatcherTimer (Thread timer)
Each of these classes has been designed and optimized for use in different situations. This article describes
System.Timers.Timer class and helps you gain an understanding of how this class should be used. To know more about other timer class please look into
here.
↑ Return to Top
Schedule a Task
Scheduling a task in code this term means any code that does something, causes something to happen, and has action to it. Example:
- Making an object appear
- Making an object move
- Fire any event
- Trigger functions
Timer counts automatically. When the timer counts down the amount of time set in the preferences, it executes whatever code is in it, then it automatically restarts and counts down again.
Basic Functions
Some of the functions that are used in this project.
- Timer.Enabled: «Whether the Timer should raise the Elapsed event.» This must set this to true if we want timer to do anything.
- Timer.Interval: Interval of the time is count in milliseconds, between raisings of the Elapsed event. The default is 100 milliseconds.» We must make this interval longer than the default. For example, for 60 seconds or 1 minute, use 60000
as the Interval. - Timer.Start: This does the same thing as setting Enabled to true. Starts raising the Elapsed event by setting Enabled to true.
- Timer.Stop: This does the same thing as setting Enabled to false. Stops raising the Elapsed event by setting Enabled to false.
- Timer.Tick: Occurs when the specified timer interval has elapsed and the timer is enabled.
- Timer.Elapsed: This is the event that is invoked each time the Interval of the Timer has passed.
Code Usage
using
System;
using
System.Timers;
namespace
ScheduleTimer
{
class
Program
{
static
Timer timer;
static
void
Main(string
[] args)
{
schedule_Timer();
Console.ReadLine();
}
static
void
schedule_Timer()
{
Console.WriteLine(
"### Timer Started ###"
);
DateTime nowTime = DateTime.Now;
DateTime scheduledTime =
new
DateTime(nowTime.Year, nowTime.Month, nowTime.Day, 8, 42, 0, 0); //Specify your scheduled time HH,MM,SS [8am and 42 minutes]
if
(nowTime > scheduledTime)
{
scheduledTime = scheduledTime.AddDays(1);
}
double
tickTime = (
double
)(scheduledTime - DateTime.Now).TotalMilliseconds;
timer =
new
Timer(tickTime);
timer.Elapsed +=
new
ElapsedEventHandler(timer_Elapsed);
timer.Start();
}
static
void
timer_Elapsed(object
sender, ElapsedEventArgs e)
{
Console.WriteLine(
"### Timer Stopped ### n"
);
timer.Stop();
Console.WriteLine(
"### Scheduled Task Started ### nn"
);
Console.WriteLine(
"Hello World!!! - Performing scheduled taskn"
);
Console.WriteLine(
"### Task Finished ### nn"
);
schedule_Timer();
}
}
}
Output
Explanation
As you can see when the timer hit the specified interval it stops and starts performing the given task, for our case just simply print «Hello World» on the console window.
If we look closer we can see that a timer object is created, then we assigned the interval in millisecond by calculating current time with scheduled time. Then we assign the associated method the timer will execute after interval.
Then Start method is called to start the interval. The timer continues to operate until it hit the specified interval. When it hit the specified interval Stop method is called to stop the timer, perform the specified task, after completing start the timer again.
Two DateTime object is created. Once is for the current date time and other is for to detecting scheduled date time. Also our code has the ability to automatically increment it’s date to next day to perform the task daily. So, no user interaction needed.
↑ Return to Top
Summary
We looked at the Timer class from the System.Timers namespace in .NET Framework. One point we may want to consider when working with timers is whether our problem can be solved more simply by using the Windows Scheduler to run a standard executable
periodically. Here timer is used to fires off a program at a given time. We could also use timer in following cases:
- Fire off a program at a given time
- Display the time on the screen
- Create a backup routine that copies important data at a given interval
- Can create a routine to automatically log off a user or end a program after a given time period with no activity
- Create a WCF service add timer to schedule a job
Beside these numerous ways timer can be used.
↑ Return to Top
Reference
- Timer class under System.Timers namespace
here - All about .NET Timers — A Comparison
here - Introduction to Server-Based Timers
here - Timers, Timer Resolution, and Development of Efficient Code
here
↑ Return to Top
See Also
- Comparing
the Timer Classes in the .NET Framework Class Library - Quartz
Enterprise Scheduler .NET - Limitations
of the Windows Forms Timer Component’s Interval Property
↑ Return to Top
Download
You can download the Source Code used in the example from this link
Download Source Code
↑ Return to Top
Вы пишите код на платформе .NET под Windows и вам нужно выполнять некоторые действия каждую миллисекунду. Возможно ли это? Какие есть варианты и насколько они надёжны? Разберёмся, что можно использовать, и какие есть гарантии по точности срабатывания. Статья сконцентрирована на поиске такого решения, которое работало бы и под .NET Framework, и под .NET Core / .NET, и в разных версиях ОС, и являлось бы механизмом общего назначения (а не только для программ с GUI, например).
Для чего вообще может потребоваться таймер с малым периодом? Примером могут служить различные программные аудио- и видеоплееры. Классический подход при воспроизведении мультимедийных данных – раз в N единиц времени смотреть, что́ нужно подать на устройство вывода (видео-, звуковую карту и т.д.) в данный момент времени, и при необходимости отсылать новые данные (кадр, аудиобуфер) на это устройство. В таких случаях информация часто расположена достаточно плотно (особенно в случае с аудио), а временны́е отклонения в её воспроизведении хорошо заметны ушам, глазам и прочим человеческим органам. Поэтому N выбирается небольшим, измеряется в миллисекундах, и часто используется значение 1.
Я разрабатываю библиотеку для работы с MIDI – DryWetMIDI. Помимо взаимодействия с MIDI файлами, их трансформации и сопряжения с музыкальной теорией, библиотека предлагает API для работы с MIDI устройствами, а также средства для воспроизведения и записи MIDI данных. DryWetMIDI написана на C#, а мультимедийный API реализован для Windows и macOS. Вкратце воспроизведение в библиотеке работает так:
-
все MIDI-события снабжаются временем, когда они должны быть воспроизведены, время измеряется в миллисекундах и отсчитывается от начала всех данных (т.е. от 0);
-
указатель
P
устанавливается на первое событие; -
запускается счётчик времени
C
; -
запускается таймер
T
с интервалом 1 мс; -
при каждом срабатывании
T
: a) если время воспроизведения текущего события (на которое указываетP
) меньше или равно текущему времени, взятому изC
, послать событие на устройство; если нет – ждать следующего тика таймера; b) сдвинутьP
вперёд на одно событие и вернуться на a.
Итак, нам нужен таймер, срабатывающий с интервалом 1 мс. В этой статье мы посмотрим, что нам предлагает Windows для решения данной задачи, и как мы можем использовать это в .NET.
К слову, можно легко проверить, что привычные нам программные продукты для воспроизведения аудио и видео используют таймер с малым интервалом. В Windows есть встроенная утилита Powercfg, позволяющая получать данные по энергопотреблению, и в частности, какие программы запрашивают повышение разрешения (= понижение интервала) системного таймера.
Например, запустив Google Chrome и открыв любое видео в YouTube, выполните команду
powercfg /energy /output C:report.html /duration 5
В корне диска C будет создан файл с отчётом report.html. В отчёте увидим такую запись:
Platform Timer Resolution:Outstanding Timer Request
A program or service has requested a timer resolution smaller than the platform maximum timer resolution.
Requested Period 10000
Requesting Process ID 2384
Requesting Process PathDeviceHarddiskVolume3Program Files (x86)GoogleChromeApplicationchrome.exe
Браузер запросил новый период системного таймера 10000. Единицы этого значения – сотни наносекунд (как бы это ни было странно). Если перевести в миллисекунды, то получим как раз 1.
Или же при воспроизведении аудиофайла в Windows Media Player:
Platform Timer Resolution:Outstanding Timer Request
A program or service has requested a timer resolution smaller than the platform maximum timer resolution.
Requested Period 10000
Requesting Process ID 11876
Requesting Process PathDeviceHarddiskVolume3Program Files (x86)Windows Media Playerwmplayer.exe
Любопытно, что, например, VLC использует интервал 5 мс:
Platform Timer Resolution:Outstanding Timer Request
A program or service has requested a timer resolution smaller than the platform maximum timer resolution.
Requested Period 50000
Requesting Process ID 25280
Requesting Process PathDeviceHarddiskVolume3Program FilesVideoLANVLCvlc.exe
Есть подозрение (непроверенное), что частота таймера зависит от частоты кадров видео. А быть может, разработчики видеоплеера просто посчитали наглостью всегда запрашивать 1 мс. И, возможно, они правы.
Подготовка тестового кода
Создадим каркас наших тестов. Опишем интерфейс таймера:
using System;
namespace Common
{
public interface ITimer
{
void Start(int intervalMs, Action callback);
void Stop();
}
}
Метод Start
принимает первым параметром интервал таймера. Я решил проверить работу таймеров не только для интервала 1 мс, но также и для 10 и 100 мс. Вторым параметром будем передавать метод, который будет выполняться при срабатывании таймера.
Все наши проверки сделаем в одном классе:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
namespace Common
{
public static class TimerChecker
{
private static readonly TimeSpan MeasurementDuration = TimeSpan.FromMinutes(3);
private static readonly int[] IntervalsToCheck = { 1, 10, 100 };
public static void Check(ITimer timer)
{
Console.WriteLine("Starting measuring...");
Console.WriteLine($"OS: {Environment.OSVersion}");
Console.WriteLine("--------------------------------");
foreach (var intervalMs in IntervalsToCheck)
{
Console.WriteLine($"Measuring interval of {intervalMs} ms...");
MeasureInterval(timer, intervalMs);
}
Console.WriteLine("All done.");
}
private static void MeasureInterval(ITimer timer, int intervalMs)
{
var times = new List<long>((int)Math.Round(MeasurementDuration.TotalMilliseconds) + 1);
var stopwatch = new Stopwatch();
Action callback = () => times.Add(stopwatch.ElapsedMilliseconds);
timer.Start(intervalMs, callback);
stopwatch.Start();
Thread.Sleep(MeasurementDuration);
timer.Stop();
stopwatch.Stop();
var deltas = new List<long>();
var lastTime = 0L;
foreach (var time in times.ToArray())
{
var delta = time - lastTime;
deltas.Add(delta);
lastTime = time;
}
File.WriteAllLines($"deltas_{intervalMs}.txt", deltas.Select(d => d.ToString()));
}
}
}
Т.е.
-
запускаем Stopwatch;
-
в течение 3 минут складываем с него время при каждом срабатывании таймера в список;
-
собираем интервалы между собранными временами;
-
записываем полученные дельты в текстовый файл deltas_<interval>.txt.
Далее по этим наборам дельт строим графики, чтобы наглядно видеть, насколько точно срабатывает таймер. Содержимое каждого графика будет таким:
Справа сверху будет отображаться процент “хороших” результатов – дельт, попадающих в 10-процентную окрестность вокруг заданного интервала. Число 10 выбрано навскидку, но, как мы увидим, оно вполне помогает понять разницу между таймерами.
Если не сказано явно, запуск тестов производится на виртуальных машинах Azure Pipelines из пула Microsoft с операционной системой Microsoft Windows Server 2019 (10.0.17763). Иногда будем смотреть на моей локальной машине с ОС Windows 10 20H2 (сборка 19042.1348). Windows 11 под рукой нет, быть может, кому-то будет интересно проверить там.
Я решил сделать тест каждого варианта в виде отдельного консольного приложения. Все эти приложения собрал вместе в солюшне в виде проектов. Все ссылки на код, данные и графики будут приведены в конце статьи. А мы начинаем наше исследование.
EDIT ────────
В комментариях было высказано много интересных идей и предложений. Не все из них меняют итоги статьи, но для полноты картины упомянуть стоит. Кроме того, будут добавлены графики загрузки процессора. Такие вставки будут оформлены, как этот абзац.
────────────
Бесконечный цикл
Нельзя обойти стороной наивный подход – таймер на основе бесконечного цикла с подсчётом интервала:
using Common;
using System;
using System.Diagnostics;
using System.Threading;
namespace InfiniteLoopTimer
{
internal sealed class Timer : ITimer
{
private bool _running;
public void Start(int intervalMs, Action callback)
{
var thread = new Thread(() =>
{
var lastTime = 0L;
var stopwatch = new Stopwatch();
_running = true;
stopwatch.Start();
while (_running)
{
if (stopwatch.ElapsedMilliseconds - lastTime < intervalMs)
continue;
callback();
lastTime = stopwatch.ElapsedMilliseconds;
}
});
thread.Start();
}
public void Stop()
{
_running = false;
}
}
}
Запустив тест с этим таймером
using Common;
namespace InfiniteLoopTimer
{
internal class Program
{
static void Main(string[] args)
{
TimerChecker.Check(new Timer());
}
}
}
получим, разумеется, отличные результаты. Например, для 1 мс:
И хоть результаты не могут не радовать, данный таймер, конечно же, не стоит использовать в реальных приложениях. Все мы знаем, что он будет попусту загружать CPU и расходовать батарею на портативных устройствах. Например, на моей машине с 4 ядрами получим такую загрузку процессора:
То есть примерно одно ядро. Графики для других интервалов приводить не буду, там аналогичные картины (кто хочет, может посмотреть по ссылке в конце статьи).
EDIT ────────
Как ни странно, именно бесконечный цикл вызвал в комментариях наибольшую активность. Было предложено несколько вариантов, рассмотрим каждый.
Во-первых, не один человек уверенно высказался о том, что вызов метода Thread.Yield должен снизить загрузку процессора. Что ж, напишем новый таймер:
using System;
using System.Diagnostics;
using System.Threading;
using Common;
namespace InfiniteLoopTimerWithThreadYield
{
internal sealed class Timer : ITimer
{
private bool _running;
public void Start(int intervalMs, Action callback)
{
var thread = new Thread(() =>
{
var lastTime = 0L;
var stopwatch = new Stopwatch();
_running = true;
stopwatch.Start();
while (_running)
{
if (stopwatch.ElapsedMilliseconds - lastTime >= intervalMs)
{
callback();
lastTime = stopwatch.ElapsedMilliseconds;
}
if (!Thread.Yield())
Thread.Sleep(0);
}
});
thread.Start();
}
public void Stop()
{
_running = false;
}
}
}
Точность будет высокая (смотрел на своём локальном компьютере)
но вот загрузка процессора не меняется
Результаты аналогичные и для виртуалок Azure Pipelines и для второго компьютера. Т.е. совершенно точно нельзя назвать такой таймер хорошим.
Во-вторых, в комментариях были упомянуты NtSetTimerResolution
/ NtDelayExecution
. Это недокументированные функции системной библиотеки ntdll.dll. Я модифицировал бесконечный цикл простейшим образом с использованием этих функций, сделав такой таймер:
using System;
using System.Runtime.InteropServices;
using System.Threading;
using Common;
namespace InfiniteLoopTimerWithNtDelayExecution
{
internal sealed class Timer : ITimer
{
[DllImport("ntdll.dll", SetLastError = true)]
private static extern void NtSetTimerResolution(uint DesiredResolution, bool SetResolution, ref uint CurrentResolution);
[DllImport("ntdll.dll")]
private static extern bool NtDelayExecution(bool Alertable, ref long DelayInterval);
private Thread _thread;
private bool _running;
public void Start(int intervalMs, Action callback)
{
var res = (uint)(intervalMs * 10000);
NtSetTimerResolution(res, true, ref res);
_thread = new Thread(() =>
{
_running = true;
while (_running)
{
var interval = -intervalMs * 10000L;
NtDelayExecution(false, ref interval);
callback();
}
}) { Priority = ThreadPriority.Highest };
_thread.Start();
}
public void Stop()
{
_running = false;
}
}
}
Такая реализация обладает некоторыми недостатками (например тем, что срабатывания будут «плыть» в зависимости от времени выполнения callback
), но для демонстрации вполне годится. Запустив, получим такие результаты на локальной машине:
На виртуалках Azure Pipelines результаты тоже отличные, хотя среднее значение и не совпадает с запрошенным интервалом. Например, для интервала 1 мс:
То бишь такой таймер действительно неплох.
────────────
Переходим к стандартным классам таймеров в .NET.
System.Timers.Timer
Используя System.Timers.Timer
using Common;
using System;
namespace SystemTimersTimer
{
internal sealed class Timer : ITimer
{
private System.Timers.Timer _timer;
public void Start(int intervalMs, Action callback)
{
_timer = new System.Timers.Timer(intervalMs);
_timer.Elapsed += (_, __) => callback();
_timer.Start();
}
public void Stop()
{
_timer.Stop();
}
}
}
получим такие результаты:
EDIT ────────
Загрузка CPU, разумеется околонулевая, ибо таймер работает на пуле потоков:
────────────
Как видим, для малых интервалов 15.6 мс – наилучший средний показатель. Как известно, это стандартное разрешение системного таймера Windows, о чём можно подробно прочитать в документе от Microsoft под названием Timers, Timer Resolution, and Development of Efficient Code (кстати, очень интересный и полезный материал, рекомендую к прочтению):
The default system-wide timer resolution in Windows is 15.6 ms, which means that every 15.6 ms the operating system receives a clock interrupt from the system timer hardware.
А в документации по классу явно сказано:
The System.Timers.Timer class has the same resolution as the system clock. This means that the Elapsed event will fire at an interval defined by the resolution of the system clock if the Interval property is less than the resolution of the system clock.
Так что результаты не выглядят удивительными.
Документ выше датируется 16 июня 2010 года, однако не утерял своей актуальности. В нём также сказано:
The default timer resolution on Windows 7 is 15.6 milliseconds (ms). Some applications reduce this to 1 ms, which reduces the battery run time on mobile systems by as much as 25 percent.
Эта важная информация явно говорит о том, что понижение интервала системного таймера напрямую влияет на энергопотребление и время работы устройств от батареи. Возвращаясь к примеру с VLC из начала статьи, подход с 5 мс выглядит разумным.
Но всё же в некоторых ситуациях необходим период 1 мс, в частности для воспроизведения аудио- и MIDI-данных. В числе прочего в документе написано, как можно повысить разрешение системного таймера:
Applications can call timeBeginPeriod to increase the timer resolution. The maximum resolution of 1 ms is used to support graphical animations, audio playback, or video playback.
Т.е., согласно приведённому тексту, можно вызвать функцию timeBeginPeriod, запустить таймер с заданным интервалом, и даже стандартные таймеры должны срабатывать с этим интервалом. Что ж, проверим.
System.Timers.Timer + timeBeginPeriod
Код нового таймера:
using Common;
using System;
namespace SystemTimersTimerWithPeriod
{
internal sealed class Timer : ITimer
{
private System.Timers.Timer _timer;
private uint _resolution;
public void Start(int intervalMs, Action callback)
{
_timer = new System.Timers.Timer(intervalMs);
_timer.Elapsed += (_, __) => callback();
_resolution = NativeTimeApi.BeginPeriod(intervalMs);
_timer.Start();
}
public void Stop()
{
_timer.Stop();
NativeTimeApi.EndPeriod(_resolution);
}
}
}
Не буду здесь приводить код класса NativeTimeApi
, кому интересно, посмотрит его в архиве с солюшном (ссылка в конце статьи). Запускаем тест:
Увы, лучше не стало. Если немного погуглить, обнаружим, что мы не одиноки в своём горе:
-
timeBeginPeriod not working on Intel Comet Lake CPU (i5 10400H)
-
The timeBeginPeriod() function no longer changes the resolution of SetThreadpoolTimer() and CreateTimerQueueTimer() in Windows 10 2004
-
WIndows 10 timeBeginPeriod(1) not always working
Оказывается, начиная с версии Windows 10 2004 изменилось влияние функции timeBeginPeriod
на стандартные таймеры. А именно, теперь она на них не влияет. По этой теме можно почитать интересную статью – Windows Timer Resolution: The Great Rule Change. К слову, выглядит, что проблема присутствует и на более ранних версиях Windows 10.
Кстати говоря, визуально результаты всё же стали немного другими. А именно, уменьшился разброс относительно среднего значения. Это может быть случайностью, а может быть и влиянием функции timeBeginPeriod
.
EDIT ────────
Загрузка процессора:
────────────
System.Threading.Timer
Для полноты картины нужно также посмотреть, а как обстоят дела с System.Threading.Timer. Код:
using Common;
using System;
namespace SystemThreadingTimer
{
internal sealed class Timer : ITimer
{
private System.Threading.Timer _timer;
public void Start(int intervalMs, Action callback)
{
_timer = new System.Threading.Timer(_ => callback(), null, intervalMs, intervalMs);
}
public void Stop()
{
_timer.Dispose();
}
}
}
Результаты:
EDIT ────────
Процессор нагружен аналогично предыдущему таймеру:
────────────
Ожидаемо никаких отличий от System.Timers.Timer, так как в документации нам явно говорят об этом:
The Timer class has the same resolution as the system clock. This means that if the period is less than the resolution of the system clock, the TimerCallback delegate will execute at intervals defined by the resolution of the system clock…
System.Threading.Timer + timeBeginPeriod
Работа System.Threading.Timer с предварительным вызовом timeBeginPeriod
(а вдруг с этим таймером сработает):
Не сработало.
EDIT ────────
Загрузка процессора:
────────────
Multimedia timer
В Windows издревле существует API для создания мультимедийных таймеров. Использование их состоит в регистрации функции обратного вызова с помощью timeSetEvent и предварительном вызове timeBeginPeriod
. Таким образом, опишем новый таймер:
using Common;
using System;
namespace WinMmTimer
{
internal sealed class Timer : ITimer
{
private uint _resolution;
private NativeTimeApi.TimeProc _timeProc;
private Action _callback;
private uint _timerId;
public void Start(int intervalMs, Action callback)
{
_callback = callback;
_resolution = NativeTimeApi.BeginPeriod(intervalMs);
_timeProc = TimeProc;
_timerId = NativeTimeApi.timeSetEvent((uint)intervalMs, _resolution, _timeProc, IntPtr.Zero, NativeTimeApi.TIME_PERIODIC);
}
public void Stop()
{
NativeTimeApi.timeKillEvent(_timerId);
NativeTimeApi.EndPeriod(_resolution);
}
private void TimeProc(uint uID, uint uMsg, uint dwUser, uint dw1, uint dw2)
{
_callback();
}
}
}
Запустив тест, получим такие результаты:
А вот это уже интересно. Проверим на локальной машине:
Тут вообще красота. Таймер прекрасно держит заданный интервал, лишь изредка заметно отклоняясь от него (чего избежать невозможно на Windows, ибо эта ОС не является системой реального времени).
EDIT ────────
Дополним результаты графиками загрузки процессора. На локальной машине:
На виртуалке Azure Pipelines:
Нагрузка на процессор минимальная.
────────────
Итак, результаты радуют. Однако, в документации сказано, что функция timeSetEvent
устаревшая:
This function is obsolete. New applications should use CreateTimerQueueTimer to create a timer-queue timer.
Вместо неё предлагается использовать функцию CreateTimerQueueTimer. Что ж, мы, как законопослушные разработчики идём пробовать.
Timer-queue timer
Вместо мультимедийных таймеров Microsoft рекомендует использовать таймеры, созданные на специальных очередях. Можно использовать дефолтную очередь, а можно и создавать свои. Мы будем использовать дефолтную. Код нашего таймера:
using Common;
using System;
namespace TimerQueueTimerUsingDefault
{
internal sealed class Timer : ITimer
{
private IntPtr _timer;
private NativeTimeApi.WaitOrTimerCallback _waitOrTimerCallback;
private Action _callback;
public void Start(int intervalMs, Action callback)
{
_callback = callback;
_waitOrTimerCallback = WaitOrTimerCallback;
NativeTimeApi.CreateTimerQueueTimer(
ref _timer,
IntPtr.Zero,
_waitOrTimerCallback,
IntPtr.Zero,
(uint)intervalMs,
(uint)intervalMs,
NativeTimeApi.WT_EXECUTEDEFAULT);
}
public void Stop()
{
NativeTimeApi.DeleteTimerQueueTimer(IntPtr.Zero, _timer, IntPtr.Zero);
}
private void WaitOrTimerCallback(IntPtr lpParameter, bool TimerOrWaitFired)
{
_callback();
}
}
}
Здесь в параметр Flags
функции CreateTimerQueueTimer мы передаём WT_EXECUTEDEFAULT
. Чуть позже посмотрим и на другой флаг. А пока запустим тест:
Выглядит многообещающе. Проверим на локальной машине:
Как ни странно, в разных версиях Windows таймер работает по-разному. На моей Windows 10 результаты не лучше стандартных .NET таймеров.
EDIT ────────
Любопытно, будет ли отличаться загрузка процессора в разных средах. На локальной машине:
На виртуалке Azure Pipelines:
Процессор нагружается одинаково при том, что точность разная. Не буду приводить графики загрузки CPU в дальнейших разделах, там всё то же самое.
────────────
Timer-queue timer + timeBeginPeriod
Интереса ради я проверил предыдущий таймер с предварительной установкой периода системного таймера на локальной машине:
Внезапно на 10 мс неплохие результаты. Но для 1 мс всё так же плохо.
Timer-queue timer + WT_EXECUTEINTIMERTHREAD
В прошлый раз мы использовали опцию WT_EXECUTEDEFAULT
при создании таймера. Попробуем установить другую – WT_EXECUTEINTIMERTHREAD
. Результаты (по-прежнему используем локальную машину):
И хотя ничего нового, любопытно, что у таймеров на очередях очень малый разброс значений. Практически все дельты попадают в чёткий диапазон.
Timer-queue timer + WT_EXECUTEINTIMERTHREAD + timeBeginPeriod
Без лишних слов:
Глядя на графики, я всё-таки прихожу к выводу, что timeBeginPeriod
как-то да влияет на таймеры. Коридор значений для интервала 1 мс явно становится уже.
Итоги
Буду честен, рассмотрены не все варианты. Вот тут в блоке Tip перечислены ещё такие:
-
System.Windows.Forms.Timer;
-
System.Web.UI.Timer;
-
System.Windows.Threading.DispatcherTimer.
Но и это ещё не всё. В .NET 6 появился PeriodicTimer. Зоопарк разных таймеров в .NET и Windows, конечно, весьма солидный.
Но все эти таймеры не подходят. Как я писал до ката: статья сконцентрирована на поиске такого решения, которое работало бы и под .NET Framework, и под .NET Core / .NET, и в разных версиях ОС, и являлось бы механизмом общего назначения. А потому вот причины отказа от упомянутых классов (по крайней мере для нужд мультимедиа):
-
System.Windows.Forms.Timer – выполняется на UI-потоке, привязка к приложениям с GUI;
-
System.Web.UI.Timer – только .NET Framework, да и Web.UI в имени пространства имён не вяжется с решением общего назначения;
-
System.Windows.Threading.DispatcherTimer – для WPF;
-
PeriodicTimer – только с .NET 6.
А что же можно сказать о тех таймерах, что были проверены в статье? Нет смысла писать много букв, единственный надёжный вариант – мультимедийные таймеры. И хотя они давно объявлены устаревшими, только они соответствуют критериям, указанным до ката.
EDIT ────────
Видя результаты работы таймера, основанного на функциях NtSetTimerResolution
/ NtDelayExecution
должен признать, что это также отличный вариант достичь точности в 1 мс. Более того, таким образом можно достичь и большей точности, чего невозможно сделать с мультимедийными таймерами. Большое спасибо @Alexx999 и всем неравнодушным к теме!
────────────
Всем спасибо. Как и обещал, привожу ссылки:
-
код солюшна с тестовым кодом;
-
результаты с виртуальных машин Azure Pipelines;
-
результаты с локальной машины;
-
графики по результатам с виртуальных машин Azure Pipelines;
-
графики по результатам с локальной машины.
Синтаксис
-
myTimer.Interval
— устанавливает, как часто вызывается событие «Tick» (в миллисекундах) -
myTimer.Enabled
— логическое значение, которое устанавливает таймер для включения / отключения -
myTimer.Start()
— запуск таймера. -
myTimer.Stop()
— останавливает таймер.
замечания
Если вы используете Visual Studio, таймеры могут быть добавлены в виде элемента управления непосредственно в вашу форму из панели инструментов.
Многопоточные таймеры
System.Threading.Timer
— Простой многопоточный таймер. Содержит два метода и один конструктор.
Пример: таймер вызывает метод DataWrite, который пишет «многопоточность, выполненный …», по прошествии пяти секунд, а затем каждую секунду после этого, пока пользователь не нажмет Enter:
using System;
using System.Threading;
class Program
{
static void Main()
{
// First interval = 5000ms; subsequent intervals = 1000ms
Timer timer = new Timer (DataWrite, "multithread executed...", 5000, 1000);
Console.ReadLine();
timer.Dispose(); // This both stops the timer and cleans up.
}
static void DataWrite (object data)
{
// This runs on a pooled thread
Console.WriteLine (data); // Writes "multithread executed..."
}
}
Примечание. Будет опубликован отдельный раздел для утилизации многопоточных таймеров.
Change
Этот метод можно вызвать, если вы хотите изменить интервал таймера.
Timeout.Infinite
— если вы хотите запустить только один раз. Задайте это в последнем аргументе конструктора.
System.Timers
— еще один класс таймера, предоставляемый .NET Framework. Он обертывает System.Threading.Timer
.
Особенности:
-
IComponent
—IComponent
его размещение в лотке компонента Designer в Visual Studio - Свойство
Interval
вместо методаChange
-
Elapsed
event
вместоdelegate
обратного вызова -
Enabled
для запуска и остановки таймера (default value = false
) -
Start
&Stop
если вы запутались в свойствеEnabled
(выше точки) -
AutoReset
— для указания повторяющегося события (default value = true
) - Свойство
SynchronizingObject
с методамиInvoke
иBeginInvoke
для безопасных методов вызова элементов WPF и элементов управления Windows Forms
Пример, представляющий все перечисленные выше функции:
using System;
using System.Timers; // Timers namespace rather than Threading
class SystemTimer
{
static void Main()
{
Timer timer = new Timer(); // Doesn't require any args
timer.Interval = 500;
timer.Elapsed += timer_Elapsed; // Uses an event instead of a delegate
timer.Start(); // Start the timer
Console.ReadLine();
timer.Stop(); // Stop the timer
Console.ReadLine();
timer.Start(); // Restart the timer
Console.ReadLine();
timer.Dispose(); // Permanently stop the timer
}
static void timer_Elapsed(object sender, EventArgs e)
{
Console.WriteLine ("Tick");
}
}
Multithreaded timers
— используйте пул потоков, чтобы несколько потоков могли обслуживать множество таймеров. Это означает, что метод обратного вызова или Elapsed
событие может запускаться по другому потоку каждый раз, когда он вызывается.
Elapsed
— это событие всегда срабатывает вовремя, независимо от того, является ли предыдущим Elapsed
закончило событие выполнения. Из-за этого обратные вызовы или обработчики событий должны быть потокобезопасными. Точность многопоточных таймеров зависит от ОС и обычно составляет 10-20 мс.
interop
— когда вам нужна более высокая точность, используйте это и вызовите мультимедийный таймер Windows. Это имеет точность до 1 мс и определяется в winmm.dll
.
timeBeginPeriod
— сначала timeBeginPeriod
это, чтобы сообщить ОС, что вам нужна высокая точность синхронизации
timeSetEvent
— вызывать это через timeBeginPeriod
для запуска мультимедийного таймера.
timeKillEvent
— вызывать это, когда вы закончите, это останавливает таймер
timeEndPeriod
— вызов этого, чтобы сообщить ОС, что вам больше не нужна высокая точность синхронизации.
Вы можете найти полные примеры в Интернете, которые используют мультимедийный таймер, dllimport
поиск ключевых слов dllimport
winmm.dll
timesetevent
.
Создание экземпляра таймера
Таймеры используются для выполнения задач через определенные промежутки времени (до X каждые Y секунд). Ниже приведен пример создания нового экземпляра таймера.
ПРИМЕЧАНИЕ . Это относится к таймерам, использующим WinForms. Если вы используете WPF, вы можете посмотреть в DispatcherTimer
using System.Windows.Forms; //Timers use the Windows.Forms namespace
public partial class Form1 : Form
{
Timer myTimer = new Timer(); //create an instance of Timer named myTimer
public Form1()
{
InitializeComponent();
}
}
Назначение обработчика события «Tick» для таймера
Все действия, выполняемые таймером, обрабатываются в событии «Tick».
public partial class Form1 : Form
{
Timer myTimer = new Timer();
public Form1()
{
InitializeComponent();
myTimer.Tick += myTimer_Tick; //assign the event handler named "myTimer_Tick"
}
private void myTimer_Tick(object sender, EventArgs e)
{
// Perform your actions here.
}
}
Пример: использование таймера для простого обратного отсчета.
public partial class Form1 : Form
{
Timer myTimer = new Timer();
int timeLeft = 10;
public Form1()
{
InitializeComponent();
//set properties for the Timer
myTimer.Interval = 1000;
myTimer.Enabled = true;
//Set the event handler for the timer, named "myTimer_Tick"
myTimer.Tick += myTimer_Tick;
//Start the timer as soon as the form is loaded
myTimer.Start();
//Show the time set in the "timeLeft" variable
lblCountDown.Text = timeLeft.ToString();
}
private void myTimer_Tick(object sender, EventArgs e)
{
//perform these actions at the interval set in the properties.
lblCountDown.Text = timeLeft.ToString();
timeLeft -= 1;
if (timeLeft < 0)
{
myTimer.Stop();
}
}
}
Результаты в …
И так далее…
In this article, we are going to learn how to use Timer in C#. We can set a timer to generate events following a previously set interval. In addition, the Timer class does this without blocking our program’s main execution thread.
To download the source code for this article, you can visit our GitHub repository.
Let’s start.
How Does Timer Work in C#?
Let’s see an example of how to use a Timer in C#:
public class Program { public static void Main(string[] args) { var timer = new Timer(2000); timer.Elapsed += OnEventExecution; timer.Start(); Console.ReadLine(); } public static void OnEventExecution(Object? sender, ElapsedEventArgs eventArgs) { Console.WriteLine($"Elapsed event at {eventArgs.SignalTime:G}"); } }
Here, we create an instance of the Timer
class. We use the constructor to set up a 2000 milliseconds (2 seconds) interval. After that, we use the OnEventExecution
static method as our event handler and we assign it to the timer’s Elapsed
event. Finally, we call the Start()
method so the events start to fire at the defined interval. Another way to start the timer would be to set the Enabled
property to true
.
On execution, we should see the program printing a new line to the console every 2 seconds:
Elapsed event at 3/19/2022 12:20:11 PM Elapsed event at 3/19/2022 12:20:13 PM Elapsed event at 3/19/2022 12:20:15 PM
In other words, our timer raises the elapsed event repeatedly using the interval we set up initially.
If needed, we can stop a timer by calling its Stop()
method or by setting its Enabled
property to false
.
Generating Recurring Events with Timer in C#
Any newly created timer will repeatedly raise events once started. That’s because the timer’s AutoReset
property is set to true by default.
However, in a scenario where we only need our timer to raise the Elapsed
event once we should set the AutoReset
property to false
:
var timer = new Timer(2000); timer.Elapsed += OnEventExecution; timer.AutoReset = false; // Disable recurrent events. timer.Start();
This time the output consists of one line only because the event was triggered once.
Implementing Handlers
So far, we’ve been using explicitly defined methods as event handlers for our timer. However, we have more options to implement our handlers.
Whenever we deal with really simple handlers, we can use lambda syntax to shorten the implementation:
var timer = new Timer(2000); timer.Elapsed += (sender, eventArgs) => { Console.WriteLine($"Elapsed event at {eventArgs.SignalTime:G}"); }; timer.Start();
We can implement asynchronous event handlers as well:
var sw = new StringWriter(); var timer = new Timer(2000); timer.Elapsed += async (sender, eventArgs) => { await sw.WriteLineAsync($"Elapsed event at {eventArgs.SignalTime:G}"); }; timer.Start();
Exception Handling
Whenever a timer event handler throws an exception, the timer component catches it and suppresses it. For instance, the following code won’t write any exception messages to the console:
var timer = new Timer(2000); timer.Elapsed += (sender, eventArgs) => { Console.WriteLine($"Elapsed event at {eventArgs.SignalTime:G}"); throw new Exception(); }; timer.Start();
Note that we must never rely on this behavior since it is bound to change in future versions of the .NET Framework.
Wanna join Code Maze Team, help us produce more awesome .NET/C# content and get paid? >> JOIN US! <<
On the other hand, exceptions raised within asynchronous handlers will not be suppressed by the timer component:
var timer = new Timer(2000); timer.Elapsed += async (sender, eventArgs) => { await Task.Run(() => Console.WriteLine($"Elapsed event at {eventArgs.SignalTime:G}")); throw new Exception(); }; timer.Start();
This time the program will show the error message and exit.
Disposing Timers
The Timer component implements the IDisposable
interface. In most cases, timer instances in our applications will be disposed of automatically when they go out of scope. However, in certain cases, we have to do it manually:
using (var timer = new Timer(2000)) { timer.Elapsed += (sender, eventArgs) => { Console.WriteLine($"Elapsed event at {eventArgs.SignalTime:G}"); }; timer.Start(); Console.ReadLine(); // Avoids ending the timer's scope too soon }
We leverage the using
statement to dispose of our timer once we are done with it.
Timer and Windows Forms
Usually, Timer
calls the Elapsed
event handler in the system-thread pool. This may not work when the source of the event is a visual Windows Forms component like a Form, a TextBox, or a Button.
To avoid this pitfall, we must set the SynchronizingObject
property to reference the component that handles the event. This way, our Timer
will call the event handler in the same thread where the component is located.
Alternatives to Timer in C#
System.Timers
and the Timer
class have been a part of the framework since version 1.1 so it is available in whichever target framework we work with. However, the framework includes other Timer
classes, each one with a different purpose and behavior.
System.Threading.Timer executes a given method a single time after a set interval. The callback method needs to be provided in the constructor and can’t be changed.
Wanna join Code Maze Team, help us produce more awesome .NET/C# content and get paid? >> JOIN US! <<
System.Windows.Forms.Timer is a Windows Forms component suited for single-thread environments.
System.Web.UI.Timer is part of ASP.NET and performs page postbacks at regular intervals. Only available in .NET Framework.
Timer vs Stopwatch
Timer
executes events in a separate thread at a specific user-defined interval. On the other hand, Stopwatch
runs in our program’s main thread and measures the elapsed time. Stopwatch
returns a TimeSpan
struct containing the elapsed time:
var stopwatch = new Stopwatch(); stopwatch.Start(); Thread.Sleep(100); stopwatch.Stop(); Console.WriteLine($"Total milliseconds: {stopwatch.Elapsed.TotalMilliseconds:F4}"); // Total milliseconds: 108.8356 Console.WriteLine($"Total seconds: {stopwatch.Elapsed.TotalSeconds:F4}"); // Total seconds: 0.1088 Console.WriteLine($"Total minutes: {stopwatch.Elapsed.TotalMinutes:F4}"); // Total minutes: 0.0018
Although both of these classes work with time, we can clearly see that the StopWatch
class has a completely different application.
Third-party Alternatives
Let’s have a look at some free third-party libraries that can replace Timer
. In general, these options offer an array of advanced features in exchange for some added complexity.
Quartz.NET works embedded in a program or stand-alone. It offers many ways to schedule jobs beyond a simple time interval setup. It can work in clusters, enabling fail-over and load balancing setups.
Hangfire is a job scheduler that tracks jobs in persistent storage ensuring job execution. It schedules jobs based on intervals, like Timer
, but also offers many more options like job queues or batches.
Conclusion
In this article, we’ve learned what Timer in C# is and how it works. We’ve practiced creating timer event handlers and how to dispose of our timer instances.
Wanna join Code Maze Team, help us produce more awesome .NET/C# content and get paid? >> JOIN US! <<
We’ve also learned about the differences between Timer
and StopWatch
and, finally, we had a look at some popular third-party alternatives.
I have been checking out some of the possible timers lately, and System.Threading.Timer
and System.Timers.Timer
are the ones that look needful to me (since they support thread pooling).
I am making a game, and I plan on using all types of events, with different intervals, etc.
Which would be the best?
Wai Ha Lee
8,41977 gold badges60 silver badges90 bronze badges
asked Sep 13, 2009 at 3:56
0
This article offers a fairly comprehensive explanation:
«Comparing the Timer Classes in the .NET Framework Class Library» — also available as a .chm file
The specific difference appears to be that System.Timers.Timer
is geared towards multithreaded applications and is therefore thread-safe via its SynchronizationObject
property, whereas System.Threading.Timer
is ironically not thread-safe out-of-the-box.
I don’t believe that there is a difference between the two as it pertains to how small your intervals can be.
Wai Ha Lee
8,41977 gold badges60 silver badges90 bronze badges
answered Sep 13, 2009 at 3:59
David AndresDavid Andres
31.1k7 gold badges45 silver badges36 bronze badges
10
System.Threading.Timer
is a plain timer. It calls you back on a thread pool thread (from the worker pool).
System.Timers.Timer
is a System.ComponentModel.Component
that wraps a System.Threading.Timer
, and provides some additional features used for dispatching on a particular thread.
System.Windows.Forms.Timer
instead wraps a native message-only-HWND and uses Window Timers to raise events in that HWNDs message loop.
If your app has no UI, and you want the most light-weight and general-purpose .Net timer possible, (because you are happy figuring out your own threading/dispatching) then System.Threading.Timer
is as good as it gets in the framework.
I’m not fully clear what the supposed ‘not thread safe’ issues with System.Threading.Timer
are. Perhaps it is just same as asked in this question: Thread-safety of System.Timers.Timer vs System.Threading.Timer, or perhaps everyone just means that:
-
it’s easy to write race conditions when you’re using timers. E.g. see this question:
Timer (System.Threading) thread safety -
re-entrancy of timer notifications, where your timer event can trigger and call you back a second time before you finish processing the first event. E.g. see this question: Thread-safe execution using System.Threading.Timer and Monitor
answered Jan 28, 2014 at 15:47
Tim Lovell-SmithTim Lovell-Smith
14.8k14 gold badges73 silver badges92 bronze badges
2
In his book «CLR Via C#«, Jeff Ritcher discourages using System.Timers.Timer
, this timer is derived from System.ComponentModel.Component
, allowing it to be used in design surface of Visual Studio. So that it would be only useful if you want a timer on a design surface.
He prefers to use System.Threading.Timer
for background tasks on a thread pool thread.
Nate B.
94211 silver badges31 bronze badges
answered Apr 12, 2011 at 10:51
HeroHero
1,4251 gold badge10 silver badges9 bronze badges
4
Information from Microsoft about this (see Remarks on MSDN):
- System.Timers.Timer,
which fires an event and executes the code in one or more event sinks
at regular intervals. The class is intended for use as a server-based
or service component in a multithreaded environment; it has no user
interface and is not visible at runtime.- System.Threading.Timer,
which executes a single callback method on a thread pool thread at
regular intervals. The callback method is defined when the timer is
instantiated and cannot be changed. Like the System.Timers.Timer
class, this class is intended for use as a server-based or service
component in a multithreaded environment; it has no user interface and
is not visible at runtime.- System.Windows.Forms.Timer
(.NET Framework only), a Windows Forms component that fires an event
and executes the code in one or more event sinks at regular intervals.
The component has no user interface and is designed for use in a
single-threaded environment; it executes on the UI thread.- System.Web.UI.Timer
(.NET Framework only), an ASP.NET component that performs asynchronous
or synchronous web page postbacks at a regular interval.
It is interesting to mention that System.Timers.Timer
was deprecated with .NET Core 1.0, but was implemented again in .NET Core 2.0 (/ .NET Standard 2.0).
The goal with .NET Standard 2.0 was that it should be as easy as possible to switch from the .NET Framework which is probably the reason it came back.
When it was deprecated, the .NET Portability Analyzer Visual Studio Add-In recommended to use System.Threading.Timer
instead.
Looks like that Microsoft favors System.Threading.Timer
before System.Timers.Timer
.
EDIT NOTE 2018-11-15:
I had to change my answer since the old information about .NET Core 1.0 was not valid anymore.
Tolga
2,2431 gold badge24 silver badges18 bronze badges
answered Jun 21, 2016 at 19:39
ice1e0ice1e0
9397 silver badges15 bronze badges
5
One important difference not mentioned above which might catch you out is that System.Timers.Timer
silently swallows exceptions, whereas System.Threading.Timer
doesn’t.
For example:
var timer = new System.Timers.Timer { AutoReset = false };
timer.Elapsed += (sender, args) =>
{
var z = 0;
var i = 1 / z;
};
timer.Start();
vs
var timer = new System.Threading.Timer(x =>
{
var z = 0;
var i = 1 / z;
}, null, 0, Timeout.Infinite);
answered Apr 4, 2016 at 20:28
stovrozstovroz
6,7172 gold badges47 silver badges59 bronze badges
4
I found a short comparison from MSDN
The .NET Framework Class Library includes four classes named Timer,
each of which offers different functionality:
System.Timers.Timer
, which fires an event and executes the code in one or more event sinks at regular intervals. The class is intended
for use as a server-based or service component in a multithreaded
environment; it has no user interface and is not visible at runtime.
System.Threading.Timer
, which executes a single callback method on a thread pool thread at regular intervals. The callback method is
defined when the timer is instantiated and cannot be changed. Like the
System.Timers.Timer class, this class is intended for use as a
server-based or service component in a multithreaded environment; it
has no user interface and is not visible at runtime.
System.Windows.Forms.Timer
, a Windows Forms component that fires an event and executes the code in one or more event sinks at regular
intervals. The component has no user interface and is designed for use
in a single-threaded environment.
System.Web.UI.Timer
, an ASP.NET component that performs asynchronous or synchronous web page postbacks at a regular interval.
answered Oct 2, 2014 at 14:36
defaultdefault
11.3k8 gold badges66 silver badges102 bronze badges
From MSDN: System.Threading.Timer
is a simple, lightweight timer that uses callback methods and is served by thread pool threads. It is not recommended for use with Windows Forms, because its callbacks do not occur on the user interface thread. System.Windows.Forms.Timer
is a better choice for use with Windows Forms. For server-based timer functionality, you might consider using System.Timers.Timer
, which raises events and has additional features.
Source
answered Mar 29, 2016 at 14:01
Greg GumGreg Gum
31.5k34 gold badges151 silver badges214 bronze badges
The two classes are functionally equivalent, except that System.Timers.Timer
has an option to invoke all its timer expiration callbacks through ISynchronizeInvoke by setting SynchronizingObject. Otherwise, both timers invoke expiration callbacks on thread pool threads.
When you drag a System.Timers.Timer
onto a Windows Forms design surface, Visual Studio sets SynchronizingObject to the form object, which causes all expiration callbacks to be called on the UI thread.
answered Sep 9, 2015 at 10:56
Edward BreyEdward Brey
39.4k19 gold badges193 silver badges247 bronze badges
As other mentioned the link to MS Docs, one major difference between System.Timers.Timer
and System.Threading.Timer
is that System.Threading.Timer
executes a single callback method defined once while the System.Timers.Timer
reacts on events, so supports multiple subscribers which can be also removed.
As also mentioned above, the System.Timers.Timer
is using a System.Threading.Timer
internally, with e.g. the Enable=false disposing the internal timer, and re creates it on Enable=true / Start():
https://source.dot.net/#System.ComponentModel.TypeConverter/System/Timers/Timer.cs
answered Jul 15, 2021 at 9:12
EricBDevEricBDev
1,13711 silver badges18 bronze badges
Таймеры часто играют важную роль как в клиентских приложениях, так и в компонентах программ, основанных на серверах (включая службы 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, и получите следующий вывод сообщений:
//Листинг 1, вывод сообщений при использовании System.Windows.Forms.Timer: System.Windows.Forms.Timer Started @ 4:09:28 PM --> Timer Event 1 @ 4:09:29 PM on Thread: UIThread --> Timer Event 2 @ 4:09:30 PM on Thread: UIThread --> Timer Event 3 @ 4:09:31 PM on Thread: UIThread Sleeping for 5000 ms... --> Timer Event 4 @ 4:09:36 PM on Thread: UIThread System.Windows.Forms.Timer Stopped @ 4:09:37 PM
В этом демонстрационном приложении свойству 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, как показано в следующем примере кода:
//Листинг 2, пример использования System.Windows.Forms.Timer: System.Windows.Forms.Timer tmrWindowsFormsTimer = new System.Windows.Forms.Timer(); tmrWindowsFormsTimer.Interval = 1000; tmrWindowsFormsTimer.Tick += new EventHandler(tmrWindowsFormsTimer_Tick); tmrWindowsFormsTimer.Start(); ...
private void tmrWindowsFormsTimer_Tick(object sender, System.EventArgs e) { // Выполнение каких-либо действий в контексте потока UI: ... }
[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 tmrTimersTimer = new System.Timers.Timer(); tmrTimersTimer.Interval = 1000; tmrTimersTimer.Elapsed += new ElapsedEventHandler(tmrTimersTimer_Elapsed); tmrTimersTimer.SynchronizingObject = this;
//Синхронизация с текущей формой...
tmrTimersTimer.Start();
...
private void tmrTimersTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) { // Выполнение каких-то действий в контексте потока UI (тот же поток, // в котором была создана форма): ... // Если мы не установили свойство SynchronizingObject, то код выполняется // в контексте текущего потока (worker thread). ... }
В листинге 3 показан пример, как использовать свойство SynchronizingObject. Вы можете использовать демонстрационное приложение, чтобы проанализировать поведение класса System.Timers.Timer. Для этого переведите радиокнопку в во вторую позицию, и выполните у же последовательность действий, которая была проделана ранее в обсуждении тестирования класса System.Windows.Forms.Timer. Вы увидите приблизительно такие сообщения:
//Листинг 4, тестирование класса System.Timers.Timer: System.Timers.Timer Started @ 5:15:01 PM --> Timer Event 1 @ 5:15:02 PM on Thread: WorkerThread --> Timer Event 2 @ 5:15:03 PM on Thread: WorkerThread --> Timer Event 3 @ 5:15:04 PM on Thread: WorkerThread Sleeping for 5000 ms... --> Timer Event 4 @ 5:15:05 PM on Thread: WorkerThread --> Timer Event 5 @ 5:15:06 PM on Thread: WorkerThread --> Timer Event 6 @ 5:15:07 PM on Thread: WorkerThread --> Timer Event 7 @ 5:15:08 PM on Thread: WorkerThread --> Timer Event 8 @ 5:15:09 PM on Thread: WorkerThread System.Timers.Timer Stopped @ 5:15:10 PM
Как Вы можете видеть, здесь не были пропущены события таймера даже тогда, когда поток 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 перегружаемых конструктора. Ниже показано, что это значит:
public Timer (TimerCallback callback, object state, long dueTime, long period);
public Timer (TimerCallback callback, object state, UInt32 dueTime, UInt32 period);
public Timer (TimerCallback callback, object state, int dueTime, int period);
public Timer (TimerCallback callback, object state, TimeSpan dueTime, TimeSpan period);
Первый параметр (callback, функция обратного вызова) требует делегата TimerCallback, который указывает на метод со следующей сигнатурой:
public void TimerCallback (object state);
Второй параметр (state) может быть либо null, либо объектом, который содержит информацию, зависящую от приложения. Этот объект state передается в callback-функцию Вашего таймера всякий раз, когда возникает событие таймера. Имейте в виду, что callback-функция запускается в контексте рабочего потока (worker thread), так что Вы можете гарантировать, что имеется потокобезопасный способ доступа к объекту state.
Третий параметр (dueTime) позволяет указать, когда должно быть запущено начальное событие таймера. Вы можете указать 0, чтобы запустить таймер немедленно, или предотвратить таймер от автоматического запуска, если укажете здесь значение константы System.Threading.Timeout.Infinite.
Четвертый параметр (period) задает интервал (в миллисекундах), через который должна быть вызвана callback-функция. Если указать 0 или Timeout.Infinite, то это запретит последующие запуски событий таймера.
Как только конструктор был вызван, Вы все еще можете изменить настройки dueTime и period использованием метода Change. У этого метода также имеется четыре перезагрузки:
public bool Change (int dueTime, int period);
public bool Change (uint dueTime, uint period);
public bool Change (long dueTime, long period);
public bool Change (TimeSpan dueTime, TimeSpan period);
Ниже показан пример кода для запуска и остановки этого таймера (подобный код используется в демонстрационном приложении):
//Таймер инициируется так, что он не запустится автоматически: System.Threading.Timer tmrThreadingTimer = new System.Threading.Timer(new TimerCallback(tmrThreadingTimer_TimerCallback), null, System.Threading.Timeout.Infinite, 1000); ...
//Ручной запуск таймера: tmrThreadingTimer.Change(0, 1000);
//Ручная остановка таймера:
tmrThreadingTimer.Change(Timeout.Infinite, Timeout.Infinite);
Как Вы могли бы ожидать, запуск демонстрационного приложения для класса System.Threading.Timer даст тот же вывод, который мы видели с классом System.Timers.Timer. Из-за того, что функция TimerCallback вызывается в контексте worker thread, нет пропущенных срабатываний таймера (подразумевается, что рабочие потоки могут запуститься). Листинг 5 показывает вывод приложения при тестировании System.Threading.Timer:
//Листинг 5, тестирование класса System.Threading.Timer: System.Threading.Timer Started @ 7:17:11 AM --> Timer Event 1 @ 7:17:12 AM on Thread: WorkerThread --> Timer Event 2 @ 7:17:13 AM on Thread: WorkerThread --> Timer Event 3 @ 7:17:14 AM on Thread: WorkerThread Sleeping for 5000 ms... --> Timer Event 4 @ 7:17:15 AM on Thread: WorkerThread --> Timer Event 5 @ 7:17:16 AM on Thread: WorkerThread --> Timer Event 6 @ 7:17:17 AM on Thread: WorkerThread --> Timer Event 7 @ 7:17:18 AM on Thread: WorkerThread --> Timer Event 8 @ 7:17:19 AM on Thread: WorkerThread System.Threading.Timer Stopped @ 7:17:20 AM
В отличие от класса System.Timers.Timer, здесь нет аналога свойства SynchronizingObject, который был предоставлен классом System.Timers.Timer. Любые операции, которые потребуют доступа к элементам управления пользовательским интерфейсом (UI controls), должны быть корректно маршалированы с использованием методов Invoke или BeginInvoke элементов управления.
[Потокобезопасное программирование с использованием таймеров]
Для максимального повторного использования кода демонстрационное приложение вызывает один и тот же метод ShowTimerEventFired из всех трех разных типов событий таймера. Вот эти 3 обработчика события:
private void tmrWindowsFormsTimer_Tick (object sender, System.EventArgs e) { ShowTimerEventFired (DateTime.Now, GetThreadName()); }
private void tmrTimersTimer_Elapsed (object sender, System.Timers.ElapsedEventArgs e) { ShowTimerEventFired (DateTime.Now, GetThreadName()); }
private void tmrThreadingTimer_TimerCallback (object state) { ShowTimerEventFired (DateTime.Now, GetThreadName()); }
Как Вы можете видеть, метод 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:
private delegate void ShowTimerEventFiredDelegate (DateTime eventTime, string threadName);
ShowTimerEventFiredDelegate позволяет методу ShowTimerEventFired вызвать самого себя обратно в поток UI. Листинг 6 показывает код, который все это делает.
//Листинг 6, метод ShowTimerEventFired:
private void ShowTimerEventFired (DateTime eventTime, string threadName) { //InvokeRequired будет true, когда используется // System.Threading.Timer или System.Timers.Timer // (без SynchronizationObject)... if (lstTimerEvents.InvokeRequired) { // Маршалинг этого callback к потоку UI (через // экземпляр формы)... BeginInvoke (new ShowTimerEventFiredDelegate(ShowTimerEventFired), new object[] {eventTime, threadName}); } else lstTimerEvents.TopIndex = lstTimerEvents.Items.Add( String.Format("—> Timer Event {0} @ {1} on Thread: {2}", ++_tickEventCounter, eventTime.ToLongTimeString(), threadName)); }
Очень просто определить, можете ли Вы безопасно получить доступ к элементу управления 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). Если код Вашего обработчика события таймера занимает для своего выполнения больше времени, чем интервал, с которым таймер генерирует события, и Вы не предприняли необходимые меры предосторожности защиты многопоточного доступа к Вашим объектам, то тогда можете столкнуться с весьма сложными проблемами в отладке. Взгляните на следующий фрагмент кода:
private int tickCounter = 0;private void tmrTimersTimer_Elapsed (object sender, System.Timers.ElapsedEventArgs e) { System.Threading.Interlocked.Increment(ref tickCounter); Thread.Sleep(5000); MessageBox.Show(tickCounter.ToString()); }
Предположим, что свойство Interval таймера было установлено на 1000 миллисекунд, и Вы возможно будете удивлены, что первый всплывший message box покажет значение 5. Это произошло потому, что во время 5 секунд, когда событие первого таймера спало, таймер продолжал генерировать события Elapsed на других рабочих потоках (worker threads). Таким образом, значение переменной tickCounter было инкрементировано 5 раз до того, как было завершена обработка первого события таймера. Обратите внимание, как автор использовал метод Interlocked.Increment для инкремента переменной tickCounter способом, безопасным для следы многопоточного выполнения. Есть и другие способы сделать то же самое, но метод Interlocked.Increment был специально разработан для такого рода операций.
Один из простых способов разрешить проблему реентантности такого типа — обернуть обработчик прерывания таймера в блок кода, который временно запрещает и затем разрешает таймер, как показано в следующем примере:
private void tmrTimersTimer_Elapsed (object sender, System.Timers.ElapsedEventArgs e) { tmrTimersTimer.Enabled = false; System.Threading.Interlocked.Increment(ref tickCounter); Thread.Sleep(5000); MessageBox.Show(tickCounter.ToString()); tmrTimersTimer.Enabled = true; }
В этом примере кода 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).
[Ссылки]
1. Comparing the Timer Classes in the .NET Framework Class Library (статья в журнале MSDN, февраль 2004 года, автор Alex Calvo, acalvo@hotmail.com)
2. 161017TimersinNET.zip.
3. Windows Forms: Give Your .NET-Based Application a Fast and Responsive UI with Multiple Threads site:microsoft.com.
4. Programming the Thread Pool in the .NET Framework: Using Timers .NET Framework Class Library site:cnblogs.com.