Уровень сложностиСредний
Время на прочтение14 мин
Количество просмотров25K
Вы пишите код на платформе .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 Path\Device\HarddiskVolume3\Program Files (x86)\Google\Chrome\Application\chrome.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 Path\Device\HarddiskVolume3\Program Files (x86)\Windows Media Player\wmplayer.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 Path\Device\HarddiskVolume3\Program Files\VideoLAN\VLC\vlc.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;
-
графики по результатам с локальной машины.
If you want to count time or execute a method repeatedly at regular intervals, timers offer an easy solution. Compared with other solutions, like threading, timers won’t tie up a thread and are relatively accurate. The .NET provides five! different timers. Each of those is to be used differently depending on the situation. The timers which we have in our toolbox are the following:
System.Windows.Forms.TimerSystem.Windows.Threading.DispatcherTimerSystem.Threading.TimerSystem.Timers.TimerPeriodicTimer(from .NET6 )
Single-threaded timers
The System.Windows.Forms.Timer and System.Windows.Threading.DispatcherTimer are for Windows Forms and WPF respectively and are not to be used outside these environments. They expose Start and Stop methods, a Tick event that behaves like the Elapsed event in the System.Timers.Timer class, but they differ in how they are implemented. Instead of using a different thread, they use the same thread that created them, in these cases probably the UI thread. That means they are not multithreaded. One thread is responsible both for the UI and the timer and because of that, the event handler for those timers must execute fast or we risk the chance of having the UI unresponsive for some time.
Their precision, is a little less accurate than the multi threaded timers because they can be delayed while other code is executing.
Multi-Threaded timers
The precision of the multi-threaded timers is comparable to the single-threaded timers. That is, they have accuracy in the 10 to 20 milliseconds range. Usually, they are a little more accurate than the single-thread timers, due to the fact that they are executed in a different thread.
The System.Threading.Timer
The System.Threading.Timer, is the simplest timer C# has to offer. It has a constructor and two methods: Change and Dispose/DisposeAsync. Its usage is pretty simple:
using System;
using System.Threading;
Timer timer = new Timer (WriteTime!, null, 3000, 1000);
Console.ReadLine();
timer.Dispose(); // Stop and clean up the timer.
void WriteTime(object data) =>
Console.WriteLine(DateTime.Now.ToString("ss.fff"));
The above code will start displaying seconds and milliseconds, three seconds after starting and each second after that. The timer interval, can be changed later, by calling the Change method and if we need the timer to execute just one time, then instead of adding a number for the repeat interval in in the constructor, we should pass the Timeout.Infinite constant as the last parameter.
The System.Timers.Timer
The System.Timers.Timer is actually a wrapper over the System.Threading.Timer that adds additional functionality.
- It has an
Intervalproperty instead of the Change method. - uses an
Elapsedevent instead of a callback delegate at the constructor. - The timer can start and stop by using the
Enabledproperty or itsStartandStopmethods. - Has an
AutoResetflag for repeating, that defaults to true. If you want to use it only once this flag must be set to false. - There is a
Closemethod that releases the resources used by the timer that is also calling itsDisposemethod. - Finally a
SynchronizingObjectProperty that gets or sets the object used to marshal event-handler calls that are issued when an interval has elapsed.
Its usage is also very simple:
using System;
var timer = new System.Timers.Timer();
timer.Interval = 1000;
timer.Elapsed += WriteTime!;
timer.Start();
Console.ReadLine();
timer.Stop();
Console.ReadLine();
timer.Elapsed -= WriteTime!;
timer.Elapsed += WriteTimeAgain!;
timer.Start();
Console.ReadLine();
timer.Dispose();
void WriteTime(object sender, EventArgs e) =>
Console.WriteLine(DateTime.Now.ToString("ss.fff"));
void WriteTimeAgain(object sender, EventArgs e) =>
Console.WriteLine(DateTime.Now.ToString("mm.ss.fff"));
For the multi-threaded timers the callback method or Elapsed event can fire on a different thread each time it is called. For this reason we can use the SynchronizingObject property in the System.Timers.Timer to set it to the UI object we need.
Timers and memory leaks
Timers can cause memory leaks if they are not disposed, that’s why they implement the Dispose method. Consider the following:
using System.Timers;
class MemoryLeak
{
Timer timer;
MemoryLeak()
{
timer = new System.Timers.Timer { Interval = 1000 };
timer.Elapsed += WriteTime;
timer.Start();
}
void WriteTime(object sender, EventArgs e) =>
Console.WriteLine(DateTime.Now.ToString("ss.fff"));
}
the MemoryLeak class instance, can never be garbage collected, because the runtime holds references to active timers so that their events can be fired. That means that the runtime will keep alive the timer and by extension the timer will keep the MemoryLeak instance alive because of the timer.Elapsed event handler.
The System.Threading.Timer however is a different case. The runtime doesn’t hold references to the timer in the System.Threading.Timer namespace. Instead references the callback delegate.
That means that our timer can be garbage collected, before it has done the job we need it for. As an example:
static void Main()
{
var timer = new System.Threading.Timer (WriteTime, null, 3000, 1000);
GC.Collect();
System.Threading.Thread.Sleep (20000);
}
void WriteTime(object data) =>
Console.WriteLine(DateTime.Now.ToString("ss.fff"));
This code, if compiled in a release build, won’t execute as we expect. In a debug build, the timer will be kept alive till the end of the scope for debugging purposes, but in a release build, with optimizations enabled the Garbage Collector will collect the timer before it executes the WriteTime method. This can be fixed by informing the runtime that we are actually using the timer and we need it to be disposed only when we are done with it, like this:
using (var timer = new System.Threading.Timer (WriteTime, null, 3000, 1000))
{
GC.Collect();
System.Threading.Thread.Sleep (20000);
}
The PeriodicTimer
The PeriodicTimer introduced in .NET 6, exists to solve the above complications and to help with asynchronous looping. Because of async/await we can have code that doesn’t use the above timers, but repeats time like this:
using System;
using System.Threading.Tasks;
RepeatForEver();
Console.ReadLine();
async void RepeatForEver()
{
while (true)
{
await Task.Delay (1000);
Console.WriteLine(DateTime.Now.ToString("ss.fff"));
}
}
If we call the RepeatForEver method from a UI thread, then this will behave as a single-threaded timer because the await will always return on the same synchronization context. This can be solved by using .ConfigureAwait(false), but the real problem here (other that it repeats forever, this is just for the example) is that we will always have a few milliseconds loss, because of the time that takes for the other statements to execute. That means that the Console.WriteLine statement won’t execute every second, but every second plus a few milliseconds. For many repeats, that loss in accuracy adds up and can be a problem.
The solution to the above is the PeriodicTimer class. Consider the following code:
using System;
using System.Threading;
var timer = new PeriodicTimer (TimeSpan.FromSeconds (1));
RepeatForEver();
Console.ReadLine();
timer.Dispose();
async void RepeatForEver()
{
while (await timer.WaitForNextTickAsync())
Console.WriteLine(DateTime.Now.ToString("ss.fff"));
}
This code will still have a few milliseconds difference from a round second, but in each repeat PeriodicTimer will try to compensate for the difference in time. An example output will be like this:
30.220
31.216
32.216
33.219
34.220
35.221
36.222
37.223
38.220
39.216
40.215
41.229
42.214
43.215
44.216
45.219
46.220
47.221
48.217
49.223
As we see the PeriodicTimer will tick around 220 ms, each time adding or removing some ms to keep itself around the 1 second range.
The Dispose method can be used with an active WaitForNextTickAsync to interrupt it, by causing it to return false.
A restriction of the PeriodicTimer, is that only one call to WaitForNextTickAsync can exist at any given moment.
The StopWatch class
The Stopwatch is an accurate measurement of time by using the highest resolution mechanism that the operating system and the hardware can provide. It has a Start and a Stop method and an IsRunning property to return the current state of the stopwatch. The elapsed time, can be found by using its properties: Elapsed, ElapsedMilliseconds and ElapsedTicks.
The IsHighResolution field returns true if the timer is based on a high-resolution performance counter and false if it is based on the system timer. The Frequency field returns the number of ticks per second.
Mostly is being used to measure the execution time of some piece of code. Because the Stopwatch class doesn’t provide any events, it must be polled, which will result in loss of accuracy for scheduling tasks. Even if it is wrapped in a class to provide events, it will need a self-correcting algorithm for that loss of accuracy. Because of that, for scheduling tasks, the PeriodicTimer is better, as it already provides all the above. In contrast, for counting accurately time from point A to point B in our code, the StopWatch is preferred.
Interop with winmm.dll
If we want a more accurate timer, from the available ones, that have accuracy about 10 to 20 milliseconds, we can use native interop and call the Windows multimedia timer which has about one millisecond precision. We have to call timeBeginPeriod to inform windows that we need high accuracy and then call timeSetEvent. After that, timeKillEvent will stop the timer and timeEndPeriod will inform windows that high precision isn’t needed anymore.
Beware that this will decrease the performance, because windows will need to give priority to the timer and the timer may not be as accurate as we expect, depending on what other programs are running. For more info on that see: Why are the Multimedia Timer APIs (timeSetEvent) not as accurate as I would expect?
And that’s it about the different timers in C#. I hope you found this info useful and thank you for reading. As always for questions and comments, use the comments section, or contact me directly via the contact form or email, also if you don’t want to miss any of the new articles, you can always subscribe to my newsletter or the RSS feed.
Таймеры часто играют важную роль как в клиентских приложениях, так и в компонентах программ, основанных на серверах (включая службы 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.
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
Elapsedevent 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
usingdirective 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.
Last modified on:
Синтаксис
-
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 -
Elapsedeventвместо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();
}
}
}
Результаты в …
И так далее…
