Как выбирать между тремя таймерами .NET - PeriodicTimer/Timer/DispatcherTimer

· · C#, .NET, WPF, Таймер, Проектирование

В прошлой статье Практическое руководство по максимальному приближению к мягкому реальному времени на обычном Windows - чек-лист, с которого стоит начать мы разобрали, как избегать периодических циклов, полагающихся на Sleep, и вместо этого использовать событийный подход и waitable timer.

А как быть в более повседневной разработке .NET-приложений? Здесь легко запутаться в PeriodicTimer, System.Threading.Timer и DispatcherTimer.

Все они называются таймерами, но их характер довольно сильно различается:

  • таймер, у которого тик ожидается через await;
  • таймер, у которого callback прилетает через ThreadPool;
  • таймер, работающий в Dispatcher UI-потока.

На практике чаще всего смешивают примерно следующее.

  • Передают 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)

Содержание

  1. Сначала вывод (в двух словах)
  2. Сначала на одном листе
    • 2.1. Общая картина
    • 2.2. Первая таблица решений
  3. Что стоит различать в первую очередь
    • 3.1. Callback-тип или тип, ожидающий тик
    • 3.2. Работает на ThreadPool или на UI-потоке
    • 3.3. Периодическая обработка и гарантия точности — разные темы
  4. Типовые паттерны
    • 4.1. Для async-периодической обработки — PeriodicTimer
    • 4.2. Для лёгких callback’ов на ThreadPool — System.Threading.Timer
    • 4.3. Для обновления UI в WPF — DispatcherTimer
    • 4.4. Для периодической обработки, близкой к мягкому реальному времени, — другие инструменты
  5. Частые антипаттерны
  6. Чек-лист для ревью
  7. Краткая шпаргалка по выбору
  8. Итог
  9. Список источников

1. Сначала вывод (в двух словах)

  • Если хочется естественно писать обработку с фиксированным интервалом на базе await — сначала PeriodicTimer
  • Если хочется периодически запускать лёгкий callback на ThreadPool — System.Threading.Timer
  • Если хочется обновлять экран в UI-потоке WPF — DispatcherTimer
  • Вызовы System.Threading.Timer могут накладываться друг на друга. Если небрежно засунуть туда асинхронную обработку, легко получить беспорядок
  • DispatcherTimer позволяет напрямую трогать UI, но взамен тяжёлая обработка внутри легко останавливает и сам UI
  • В контексте прошлой статьи про мягкое реальное время эти три инструмента не являются главными героями высокоточного ожидания

Иначе говоря, сначала стоит смотреть на следующие три вещи.

  1. В каком потоке / контексте вы хотите это запускать
  2. Хотите ли вы записать тело обработки последовательно через async / await
  3. Допустимо ли наложение callback’ов

Разделение этих трёх вопросов заметно снижает путаницу.

2. Сначала на одном листе

2.1. Общая картина

ДаНетДаНетДаНетХочется делать что-то с фиксированным интерваломХотите запускать в UI-потоке?DispatcherTimerХотите писать телообработки прямо черезasync / await?PeriodicTimerХотите гонять лёгкийcallback наThreadPool?System.Threading.TimerРассмотреть другой дизайн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.

Есть два момента, на которые стоит обратить внимание.

  1. Используйте таймер по схеме «один таймер — один потребитель»
  2. Решите сами, какова политика на случай, когда обработка занимает дольше периода

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-таймера не различие в названиях, а вот эти три момента.

  1. Где он выполняется
  2. В каком потоке кода вы хотите его писать
  3. Как вы обрабатываете наложение и отставание

В качестве политики этого одного уже достаточно, чтобы уверенно действовать.

  1. Async-периодическая обработка — PeriodicTimer
  2. Лёгкие callback’ы на ThreadPool — System.Threading.Timer
  3. Обновление UI в WPF — DispatcherTimer
  4. Если главное — точность, смотрите другие способы ожидания

Таймеры смешиваются в голове, потому что названия похожи. Но их роли не так уж похожи.

  • PeriodicTimer — инструмент для оформления async-потока;
  • System.Threading.Timer — инструмент для периодического толчка callback’ов;
  • DispatcherTimer — инструмент для периодического обновления в UI-потоке.

Одно только раздельное осмысление этих трёх заметно успокаивает код.

И наоборот, когда они смешиваются, происходят вполне обычные неприятности:

  • то, что должно было быть async, превращается в подобие async void;
  • программа падает из-за прямого обращения к UI;
  • callback’и накладываются друг на друга, и состояние мутнеет;
  • в разговор примешивается ещё и тема точности периода.

Начните с вопроса «где вы хотите это запускать». Одного этого уже достаточно, чтобы выбор таймера стал значительно спокойнее.

9. Список источников

Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.

Спящий режим, гибернация, Modern Standby и долго работающие приложения — как проектированием предотвратить «остановилось ночью»

Разбираем, почему долго работающее Windows-приложение оказывается «остановленным к утру», начиная с различий между спящим режимом S3, гиб...

Значки в области уведомлений и всплывающие (toast) уведомления в Windows-приложениях — подводные камни NotifyIcon и выбор правильного AppNotification

Практическое руководство о том, как удерживать бизнес-приложение Windows в области уведомлений (system tray) и оповещать пользователя с п...

Эти страницы показывают тему статьи в более широком контексте услуг и решений.

Статья напрямую связана со следующими услугами.

Частые вопросы

Вопросы, которые часто возникают при консультациях по теме статьи.

В чём разница между 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, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.

Публичные ссылки

Вернуться в блог