В прошлой статье Практическое руководство по максимальному приближению к мягкому реальному времени на обычном Windows - чек-лист, с которого стоит начать мы разобрали, как избегать периодических циклов, полагающихся на Sleep, и вместо этого использовать событийный подход и waitable timer.
А как быть в более повседневной разработке .NET-приложений?
Здесь легко запутаться в PeriodicTimer, System.Threading.Timer и DispatcherTimer.
Все они называются таймерами, но их характер довольно сильно различается:
- таймер, у которого тик ожидается через
await; - таймер, у которого callback прилетает через ThreadPool;
- таймер, работающий в
DispatcherUI-потока.
На практике чаще всего смешивают примерно следующее.
- Передают
async-лямбду вSystem.Threading.Timer, хотя периодическая обработка на самом деле асинхронная - Напрямую трогают экран из таймера на ThreadPool, хотя речь идёт об обновлении UI в WPF
- Помещают тяжёлую обработку в
DispatcherTimer, замедляя весь экран целиком - В голове смешиваются прошлый разговор про «мягкое реальное время» и обычное периодическое выполнение в приложении
В этой статье мы исходим в основном из общих приложений на C# / .NET версии 6 и новее и разбираем PeriodicTimer / System.Threading.Timer / DispatcherTimer в том порядке, который меньше всего сбивает с толку в повседневной практике.
Ожидаемая область применения — это примерно следующее.
- worker / фоновые сервисы
- консольные приложения
- закулисная обработка в ASP.NET Core
- десктопные приложения на WPF
Под DispatcherTimer в этой статье в основном понимается WPF-класс System.Windows.Threading.DispatcherTimer.
В WinUI / UWP есть DispatcherTimer с тем же принципом действия.
Для WinForms в качестве UI-таймера естественнее смотреть на System.Windows.Forms.Timer.
Отметим, что здесь мы разбираем именно то, как писать периодическое выполнение на стороне приложения. Когда сама точность периода становится главной темой, речь возвращается к прошлой статье про мягкое реальное время.
Также код, который встречается в этой статье, опубликован на GitHub как полный набор сборно-запускаемых примеров (библиотеки и консольные демонстрации для PeriodicTimer / System.Threading.Timer, а также модульные тесты, проверяющие свёртку тиков и наложение callback’ов).
periodictimer-system-threading-timer-dispatchertimer-guide - komurasoft-blog-samples (GitHub)
Содержание
- Сначала вывод (в двух словах)
- Сначала на одном листе
- 2.1. Общая картина
- 2.2. Первая таблица решений
- Что стоит различать в первую очередь
- 3.1. Callback-тип или тип, ожидающий тик
- 3.2. Работает на ThreadPool или на UI-потоке
- 3.3. Периодическая обработка и гарантия точности — разные темы
- Типовые паттерны
- 4.1. Для async-периодической обработки —
PeriodicTimer - 4.2. Для лёгких callback’ов на ThreadPool —
System.Threading.Timer - 4.3. Для обновления UI в WPF —
DispatcherTimer - 4.4. Для периодической обработки, близкой к мягкому реальному времени, — другие инструменты
- 4.1. Для async-периодической обработки —
- Частые антипаттерны
- Чек-лист для ревью
- Краткая шпаргалка по выбору
- Итог
- Список источников
1. Сначала вывод (в двух словах)
- Если хочется естественно писать обработку с фиксированным интервалом на базе
await— сначалаPeriodicTimer - Если хочется периодически запускать лёгкий callback на ThreadPool —
System.Threading.Timer - Если хочется обновлять экран в UI-потоке WPF —
DispatcherTimer - Вызовы
System.Threading.Timerмогут накладываться друг на друга. Если небрежно засунуть туда асинхронную обработку, легко получить беспорядок DispatcherTimerпозволяет напрямую трогать UI, но взамен тяжёлая обработка внутри легко останавливает и сам UI- В контексте прошлой статьи про мягкое реальное время эти три инструмента не являются главными героями высокоточного ожидания
Иначе говоря, сначала стоит смотреть на следующие три вещи.
- В каком потоке / контексте вы хотите это запускать
- Хотите ли вы записать тело обработки последовательно через
async/await - Допустимо ли наложение callback’ов
Разделение этих трёх вопросов заметно снижает путаницу.
2. Сначала на одном листе
2.1. Общая картина
flowchart LR
A["Хочется делать что-то с фиксированным интервалом"] --> B{"Хотите запускать в UI-потоке?"}
B -- "Да" --> C["DispatcherTimer"]
B -- "Нет" --> D{"Хотите писать тело<br/>обработки прямо через<br/>async / await?"}
D -- "Да" --> E["PeriodicTimer"]
D -- "Нет" --> F{"Хотите гонять лёгкий<br/>callback на<br/>ThreadPool?"}
F -- "Да" --> G["System.Threading.Timer"]
F -- "Нет" --> H["Рассмотреть другой дизайн<br/>Channel / BackgroundService / событие / waitable timer"]
На практике этого разветвления обычно достаточно. Когда сомневаетесь, надёжнее всего сначала разделить так: PeriodicTimer — для асинхронной обработки, DispatcherTimer — для обновления UI.
System.Threading.Timer удобен, но из-за наложения callback’ов и особенностей управления жизненным циклом он немного капризен в качестве самого первого выбора.
2.2. Первая таблица решений
| Ситуация | Первый выбор | Где выполняется | Почему подходит | На что обратить внимание сразу |
|---|---|---|---|---|
| Хочется прогонять async-обработку вроде HTTP / БД / файлового ввода-вывода с фиксированным интервалом | PeriodicTimer |
Внутри потока текущего async-метода | Пишется на базе await, остановка и отмена выглядят естественно |
Рассчитан на одну связку «таймер — потребитель». Отставание не распараллеливается само собой |
| Хочется гонять лёгкий heartbeat / отправку метрик / проверку истечения кэша на ThreadPool | System.Threading.Timer |
ThreadPool | Лёгкий, callback-стиль. Легко встроить в существующий дизайн на основе callback’ов | Callback рассчитан на реентерабельность. Возможно наложение. Нужно удерживать ссылку |
| Хочется периодически обновлять часы или лёгкий UI в WPF | DispatcherTimer |
Dispatcher WPF (UI-поток) |
Можно напрямую трогать UI. Есть приоритеты | Точное время срабатывания не гарантировано. Тяжёлая обработка забивает UI |
Точность периода — это суть, и хочется избежать циклов на Sleep |
Не делать эти три инструмента главными героями | - | Цель — не периодическое выполнение приложения, а проектирование точности ожидания | Смотреть в сторону событий / waitable timer |
В этой таблице важно смотреть не на название таймера, а на место выполнения и стиль написания. Когда выбор таймера оказывается неудачным, чаще проблема именно в том, что не посмотрели «где это выполняется», а не в названии API.
3. Что стоит различать в первую очередь
3.1. Callback-тип или тип, ожидающий тик
Разделение этого момента сразу заметно проясняет картину.
System.Threading.TimerиDispatcherTimer— callback- / событийного типаPeriodicTimer— тип, где выawait-ите ожидание тика
То есть:
- callback-тип — «таймер сам вас вызывает»;
PeriodicTimer— «вы сами ждёте следующий тик».
Если тело обработки async и вы хотите читать «ждать → обработать → снова ждать» как единый поток, PeriodicTimer естественнее.
И наоборот, в ситуациях, когда:
- нужно встроиться в существующий дизайн на основе callback’ов;
- тело обработки короткое и синхронное;
- нужен просто периодический толчок,
подходит System.Threading.Timer.
PeriodicTimer удобен, но не всесилен.
Он не рассчитан на одновременный запуск нескольких WaitForNextTickAsync для одного и того же таймера, а если тики произошли несколько раз, пока вы не ждали, они сворачиваются в один.
Здесь важно не понимать это как «само собой наверстает упущенное».
3.2. Работает на ThreadPool или на UI-потоке
Следующее, на что стоит посмотреть, — где это выполняется.
Callback у System.Threading.Timer выполняется не на создавшем его потоке, а на ThreadPool.
Это подходит для фоновой обработки, но не рассчитано на прямое обращение к UI.
DispatcherTimer, напротив, — это UI-таймер, встроенный в очередь Dispatcher.
В WPF он работает на том же Dispatcher, поэтому UI можно напрямую обновлять прямо внутри обработчика Tick.
Эта разница довольно велика.
- Чтобы обратиться к UI из таймера на ThreadPool, нужно явно вернуться в UI-поток
DispatcherTimerупрощает обращение к UI, но за счёт этого расходует время UI-потока
Иначе говоря, сила DispatcherTimer — в «безопасном обращении к UI», но это одновременно означает, что «тяжёлая обработка внутри забирает с собой и ввод, и перерисовку».
3.3. Периодическая обработка и гарантия точности — разные темы
Этот момент важен как связь с прошлой статьёй.
Формулировка «делать что-то с фиксированным интервалом» звучит одинаково, но на деле это разные проблемы:
- периодическая обработка каждые несколько секунд как удобство на уровне приложения;
- стремление максимально приблизиться к deadline на масштабе от 1 мс до нескольких мс.
System.Threading.Timer — лёгкий и удобный таймер, но не специализированный инструмент для точности.
DispatcherTimer тоже подвержен влиянию состояния очереди Dispatcher и приоритетов.
PeriodicTimer, судя по одному только названию, может показаться «должен быть точным по периоду», но на практике его сила — это удобство написания async-потока, а не точность.
Поэтому безопаснее сразу разделить, хотите ли вы:
- писать периодическое выполнение на уровне приложения, или
- добиваться точности ожидания.
Когда эти два вопроса смешиваются, разговор о выборе таймера постепенно уходит куда-то не туда.
4. Типовые паттерны
4.1. Для async-периодической обработки — PeriodicTimer
В worker’е, BackgroundService или резидентном консольном процессе, если хочется прогонять async-обработку с фиксированным интервалом, проще всего писать с помощью PeriodicTimer.
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
public sealed class CacheRefreshWorker : BackgroundService
{
private readonly ILogger<CacheRefreshWorker> _logger;
public CacheRefreshWorker(ILogger<CacheRefreshWorker> logger)
{
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("CacheRefreshWorker started.");
await RefreshCacheAsync(stoppingToken);
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(5));
try
{
while (await timer.WaitForNextTickAsync(stoppingToken))
{
await RefreshCacheAsync(stoppingToken);
}
}
catch (OperationCanceledException)
{
_logger.LogInformation("CacheRefreshWorker stopping.");
}
}
private async Task RefreshCacheAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Refreshing cache...");
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
}
}
Достоинства такой формы:
- поток кода легко проследить как единый
async-метод; CancellationTokenлегко передавать напрямую дальше вниз по цепочке;- меньше нужно управления жизненным циклом и исключениями в стиле callback’ов.
Особенно хорошо подходит, когда тело обработки сосредоточено вокруг ожидания ввода-вывода:
- вызов HTTP;
- запрос к БД;
- чтение файла;
- await других async API.
Есть два момента, на которые стоит обратить внимание.
- Используйте таймер по схеме «один таймер — один потребитель»
- Решите сами, какова политика на случай, когда обработка занимает дольше периода
PeriodicTimer не распараллеливается автоматически, чтобы наверстать упущенное, только потому что предыдущая обработка затянулась.
В этом смысле это таймер для «естественной записи async-цикла с фиксированным интервалом».
Если важна ещё и тестируемость, незаметно полезен и конструктор, принимающий TimeProvider.
4.2. Для лёгких callback’ов на ThreadPool — System.Threading.Timer
Если нужно всего лишь периодически вызывать короткий callback, System.Threading.Timer подходит без изысков.
Например, в таких сценариях:
- отправка heartbeat;
- сбор лёгких метрик;
- короткая проверка истечения срока;
- подвешивание к существующему дизайну на основе callback’ов.
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
public sealed class HeartbeatService : IHostedService, IDisposable
{
private readonly ILogger<HeartbeatService> _logger;
private Timer? _timer;
private int _running;
public HeartbeatService(ILogger<HeartbeatService> logger)
{
_logger = logger;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_timer = new Timer(OnTimer, null, TimeSpan.Zero, TimeSpan.FromSeconds(5));
return Task.CompletedTask;
}
private void OnTimer(object? state)
{
if (Interlocked.Exchange(ref _running, 1) != 0)
{
return;
}
try
{
_logger.LogInformation("Heartbeat: {Now}", DateTimeOffset.Now);
}
finally
{
Volatile.Write(ref _running, 0);
}
}
public Task StopAsync(CancellationToken cancellationToken)
{
_timer?.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
return Task.CompletedTask;
}
public void Dispose()
{
_timer?.Dispose();
}
}
Interlocked.Exchange добавлен в этом примере потому, что System.Threading.Timer не ждёт завершения предыдущего callback’а.
Это довольно важный момент.
- Callback выполняется на ThreadPool
- Callback рассчитан на реентерабельность
- Если обработка длится дольше интервала, вызовы могут накладываться
Если обработка не лёгкая, спокойнее спроектировать её так, чтобы:
- пропускать повторный запуск при наложении;
- складывать работу в очередь;
- переходить на
PeriodicTimer.
Ещё один незаметно важный момент — удерживать ссылку.
System.Threading.Timer, даже работая, становится кандидатом на сборку мусора, если на него не остаётся ссылок.
Также сразу после вызова Dispose() уже поставленные в очередь callback’ы могут всё ещё выполниться позже.
Итак, System.Threading.Timer —
- лёгкий;
- быстрый;
- простой,
но взамен это таймер, за особенности поведения callback’а которого вам приходится отвечать самостоятельно.
4.3. Для обновления UI в WPF — DispatcherTimer
Если в WPF нужно периодически обновлять часы на экране или лёгкое отображение состояния, естественным выбором будет DispatcherTimer.
using System;
using System.Windows;
using System.Windows.Threading;
public partial class MainWindow : Window
{
private readonly DispatcherTimer _clockTimer;
public MainWindow()
{
InitializeComponent();
_clockTimer = new DispatcherTimer(DispatcherPriority.Background)
{
Interval = TimeSpan.FromSeconds(1)
};
_clockTimer.Tick += ClockTimer_Tick;
_clockTimer.Start();
}
private void ClockTimer_Tick(object? sender, EventArgs e)
{
ClockText.Text = DateTime.Now.ToString("HH:mm:ss");
}
protected override void OnClosed(EventArgs e)
{
_clockTimer.Stop();
_clockTimer.Tick -= ClockTimer_Tick;
base.OnClosed(e);
}
}
Достоинство DispatcherTimer в том, что Tick обрабатывается на Dispatcher WPF, поэтому UI можно трогать напрямую.
Это хорошо подходит, например, для:
- отображения часов;
- лёгкого обновления индикатора статуса подключения;
- триггера для повторной оценки Command;
- лёгкого обновления чисел, показанных на экране.
Однако и здесь есть момент, где атмосфера меняется.
DispatcherTimer работает в UI-потоке, поэтому если поместить в обработчик Tick тяжёлую обработку, это напрямую замедлит и ввод, и отрисовку, и перекомпоновку.
Кроме того, DispatcherTimer — не инструмент, гарантирующий срабатывание «строго в указанное время».
На него влияют другая работа в очереди Dispatcher и приоритеты.
Поэтому на практике стабильность обеспечивают, если держать в уме следующее:
- содержимое Tick делать лёгким;
- тяжёлый ввод-вывод и CPU-нагрузку уводить отдельно;
- при закрытии явно обозначать конец жизненного цикла через
Stop()и отписку от события.
4.4. Для периодической обработки, близкой к мягкому реальному времени, — другие инструменты
Это точка соприкосновения с прошлой статьёй.
В прошлой статье про мягкое реальное время речь шла не о «в целом достаточно срабатывать примерно каждые столько-то секунд», а о том, как снизить дрожание периода и промахи мимо дедлайна.
В этом контексте главными темами становятся:
- не полагаться на относительное ожидание через
Sleep; - использовать событийный подход и waitable timer;
- разделять fast path и slow path;
- измерять отставание.
Поэтому чище всего сразу разделить задачи:
- обычная async-периодическая обработка в приложении
→
PeriodicTimer; - callback’и ThreadPool
→
System.Threading.Timer; - обновление UI
→
DispatcherTimer; - сама точность периода — главная тема → мир прошлой статьи.
Вопрос «хочу прогонять как можно точнее каждую 1 мс — какой .NET-таймер лучше» примерно наполовину — это уже не вопрос выбора таймера, а вопрос метода ожидания и проектирования.
5. Частые антипаттерны
5.1. Передача async-лямбды напрямую в System.Threading.Timer
Это довольно соблазнительно.
_timer = new Timer(async _ => await RefreshAsync(), null,
TimeSpan.Zero, TimeSpan.FromSeconds(5));
На вид аккуратно, но TimerCallback имеет тип void.
То есть эта async-лямбда фактически трактуется как async void.
В результате:
- вызывающая сторона не может await;
- нельзя дождаться завершения;
- управление исключениями усложняется;
- наложение callback’ов приходится продумывать отдельно —
получается довольно вязкая ситуация.
Если тело обработки async, читается понятнее, если сначала рассмотреть PeriodicTimer.
5.2. Тяжёлая обработка в Tick у DispatcherTimer
DispatcherTimer позволяет напрямую трогать UI, поэтому так и тянет писать туда всё подряд. Но это UI-поток.
Если поместить туда:
- длинную синхронную обработку;
- тяжёлые вычисления на CPU;
- блокирующий ввод-вывод;
- обработку с долгим
await, способную запускаться повторно поверх ещё не завершённой,
это напрямую столкнётся с вводом и отрисовкой UI.
Стабильнее держать содержимое Tick лёгким, уводить тяжёлую работу в фон и возвращать в UI только нужный результат.
5.3. Мнение, что PeriodicTimer сам собой наверстает отставание
Это тоже легко понять неправильно.
PeriodicTimer — отличный инструмент для чистой записи async-цикла с фиксированным интервалом, но он не запускает выполнение параллельно, чтобы самостоятельно наверстать упущенное, если предыдущая обработка затянулась.
Поскольку тики, произошедшие пока вы не ждали, могут сворачиваться в один, проектированием нужно решить:
- пропускать при отставании;
- достаточно ли смотреть только на самое свежее состояние;
- или обязательно нужно обработать каждую итерацию.
5.4. Откладывание остановки и управления жизненным циклом «на потом»
Таймеры чаще подводят не при запуске, а при остановке.
Легко упустить из виду вот что:
System.Threading.Timerсоздан как локальная переменная, и ссылка на него не удерживается;System.Threading.Timerне останавливается, а история сDispose()остаётся расплывчатой;DispatcherTimerне останавливается черезStop(), а подписка на Tick не снимается;- таймер продолжает удерживать жизненный цикл объекта даже после закрытия экрана.
Особенно DispatcherTimer может держать живым объект, к которому привязан метод-обработчик.
Если возникает странное ощущение «это Window вроде бы должно было закрыться, а почему-то всё ещё живо» — стоит заподозрить именно это.
6. Чек-лист для ревью
- Можете ли вы объяснить, как следует записать эту периодическую обработку — как обновление UI / callback ThreadPool / async-цикл?
- Не запихивается ли async-тело насильно в таймер callback-типа?
- Если используется
System.Threading.Timer, выдерживает ли код наложение callback’ов, или оно защищено отдельно? - Не содержит ли Tick у
DispatcherTimerтяжёлой обработки, блокирующего ввода-вывода, длинной синхронной работы? - Если используется
PeriodicTimer, определена ли политика на случай отставания? - Ясны ли способ остановки (
Change/Dispose/Stop) и порядок действий при завершении приложения? - Правильно ли удерживается ссылка на
System.Threading.Timer? - Есть ли отписка и уборка для
DispatcherTimerпри закрытии экрана? - Разделено ли с самого начала, о чём идёт речь — о «периодическом выполнении приложения» или о «точности ожидания»?
7. Краткая шпаргалка по выбору
Приведём практические ориентиры.
-
Хотите каждые 30 секунд обращаться к API, чтобы обновлять конфигурацию →
PeriodicTimer -
Хотите каждые 5 секунд отправлять heartbeat или лёгкие метрики →
System.Threading.Timer -
Хотите в WPF показывать часы или лёгкое обновление статуса →
DispatcherTimer -
Хотите на каждый тик напрямую трогать UI →
DispatcherTimer -
Тело периодической обработки состоит сплошь из
await, и хочется естественно обрабатывать остановку и исключения →PeriodicTimer -
Хотите дёшево добавить небольшой толчок в стиле callback →
System.Threading.Timer -
Главное — управление точностью периода и дрожанием на масштабе 1-5 мс → прежде этих трёх, смотрите способы ожидания из прошлой статьи
Если сказать очень грубо, одной строкой на каждый:
PeriodicTimer— таймер для async;System.Threading.Timer— таймер для callback’ов ThreadPool;DispatcherTimer— таймер для UI.
При таком мнемоническом правиле сильно промахнуться сложно.
8. Итог
По-настоящему важно в выборе .NET-таймера не различие в названиях, а вот эти три момента.
- Где он выполняется
- В каком потоке кода вы хотите его писать
- Как вы обрабатываете наложение и отставание
В качестве политики этого одного уже достаточно, чтобы уверенно действовать.
- Async-периодическая обработка —
PeriodicTimer - Лёгкие callback’ы на ThreadPool —
System.Threading.Timer - Обновление UI в WPF —
DispatcherTimer - Если главное — точность, смотрите другие способы ожидания
Таймеры смешиваются в голове, потому что названия похожи. Но их роли не так уж похожи.
PeriodicTimer— инструмент для оформления async-потока;System.Threading.Timer— инструмент для периодического толчка callback’ов;DispatcherTimer— инструмент для периодического обновления в UI-потоке.
Одно только раздельное осмысление этих трёх заметно успокаивает код.
И наоборот, когда они смешиваются, происходят вполне обычные неприятности:
- то, что должно было быть async, превращается в подобие
async void; - программа падает из-за прямого обращения к UI;
- callback’и накладываются друг на друга, и состояние мутнеет;
- в разговор примешивается ещё и тема точности периода.
Начните с вопроса «где вы хотите это запускать». Одного этого уже достаточно, чтобы выбор таймера стал значительно спокойнее.
9. Список источников
- Полный набор примеров кода к этой статье (библиотека, демо, модульные тесты) https://github.com/gomurin0428/komurasoft-blog-samples/tree/main/periodictimer-system-threading-timer-dispatchertimer-guide
- Связанная статья: Практическое руководство по максимальному приближению к мягкому реальному времени на обычном Windows - чек-лист, с которого стоит начать
- Связанная статья: Практическая таблица решений по async/await в C# - таблица, с которой стоит начать
- Связанная статья: Async/await и UI-поток в WPF/WinForms на одном листе - куда возвращается управление после await, Dispatcher, ConfigureAwait и где застревают .Result / .Wait()
- Timers - .NET
- PeriodicTimer Class
- PeriodicTimer.WaitForNextTickAsync(CancellationToken) Method
- PeriodicTimer.Dispose Method
- PeriodicTimer Constructor
- Timer Class (System.Threading)
- Timer Constructor (System.Threading)
- Background tasks with hosted services in ASP.NET Core
- DispatcherTimer Class (System.Windows.Threading)
- DispatcherTimer Class (Microsoft.UI.Xaml)
Похожие статьи
Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.
Зачем использовать .NET Generic Host и BackgroundService в десктопных приложениях
Разбираем, как использовать Generic Host и BackgroundService, чтобы упорядочить запуск, периодическую обработку, завершение работы, логир...
CI/CD для приложений WinForms / WPF на практике — автоматизация от сборки до подписи и распространения через GitHub Actions
Практическое руководство по настройке CI/CD для приложений WinForms / WPF через GitHub Actions. Минимальный YAML для сборки и тестов на w...
Спящий режим, гибернация, Modern Standby и долго работающие приложения — как проектированием предотвратить «остановилось ночью»
Разбираем, почему долго работающее Windows-приложение оказывается «остановленным к утру», начиная с различий между спящим режимом S3, гиб...
Значки в области уведомлений и всплывающие (toast) уведомления в Windows-приложениях — подводные камни NotifyIcon и выбор правильного AppNotification
Практическое руководство о том, как удерживать бизнес-приложение Windows в области уведомлений (system tray) и оповещать пользователя с п...
Встраиваем аутентификацию Entra ID в приложения WinForms/WPF — практическая архитектура на MSAL.NET и брокере WAM
Разбираем порядок встраивания аутентификации Entra ID в десктопные приложения WinForms/WPF: концепцию публичного клиента, регистрацию при...
Связанные темы
Эти страницы показывают тему статьи в более широком контексте услуг и решений.
Технические темы Windows
Раздел о разработке Windows, расследовании сбоев и использовании существующих активов.
Поток UI и таймеры
Поток UI WPF / WinForms, асинхронные операции, Dispatcher и проектирование таймеров.
Услуги по этой теме
Статья напрямую связана со следующими услугами.
Разработка приложений для Windows
В разработке Windows-приложений, включающих периодическое выполнение, обновление UI и фоновую обработку, выбор таймера напрямую влияет на качество реализации.
Технические консультации и ревью дизайна
Если вы на этапе, когда нужно определить, где разграничить ответственность между PeriodicTimer и DispatcherTimer, это можно проработать в формате технической консультации и ревью архитектуры.
Частые вопросы
Вопросы, которые часто возникают при консультациях по теме статьи.
- В чём разница между PeriodicTimer и System.Threading.Timer?
- Главное различие в том, что PeriodicTimer относится к типу, который ожидает тик через await, а System.Threading.Timer — к callback-типу. PeriodicTimer позволяет записать «ждать → обработать → снова ждать» как поток одного async-метода, а CancellationToken легко передаётся дальше по цепочке. Callback у System.Threading.Timer, напротив, выполняется на ThreadPool и не дожидается завершения предыдущего вызова, поэтому если обработка длится дольше интервала, вызовы могут накладываться друг на друга. Для асинхронной периодической обработки подходит PeriodicTimer, а для лёгкого синхронного периодического толчка через callback — System.Threading.Timer.
- Автоматически ли PeriodicTimer «догоняет» отставание в обработке?
- Нет. Даже если предыдущая обработка затянулась, таймер не запускает выполнение параллельно, чтобы само собой наверстать упущенное. Если за время, пока вы не ждёте, произошло несколько тиков, они сворачиваются в один. Поэтому пропускать ли при отставании, достаточно ли смотреть только на последнее состояние или обязательно нужно обработать каждую итерацию — решает проектирование. Также таймер не рассчитан на одновременный запуск нескольких WaitForNextTickAsync для одного и того же экземпляра.
- Нельзя ли передавать async-лямбду в System.Threading.Timer?
- Лучше этого избегать. TimerCallback имеет тип void, поэтому переданная async-лямбда по сути трактуется как async void. В результате вызывающая сторона не может её await, не может дождаться завершения, управление исключениями усложняется, а наложение вызовов callback приходится продумывать отдельно. Если тело обработки уже async, сначала стоит рассмотреть PeriodicTimer — это читается понятнее и безопаснее.
- Когда стоит использовать DispatcherTimer?
- Его используют, когда нужно периодически обновлять UI в WPF — например, отображение часов или лёгкий статус. Tick обрабатывается на Dispatcher WPF (UI-потоке), поэтому сильная сторона — возможность напрямую трогать UI прямо внутри обработчика. Однако именно потому, что он работает в UI-потоке, тяжёлая обработка или блокирующий ввод-вывод в Tick замедляют ввод и отрисовку заодно с ним. Также точное срабатывание в указанное время не гарантируется. Стабильнее держать содержимое Tick лёгким, уводить тяжёлую работу в фон, а при закрытии экрана явно обозначать конец жизненного цикла через Stop() и отписку от события.
Об авторе
Страница с профилем автора статьи.
Го Комура
Представитель KomuraSoft LLC
Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.
Публичные ссылки