0Pricing
C# Academy · Урок

Периодические задачи и таймеры

Планируйте повторяющиеся задачи с помощью PeriodicTimer и System.Threading.Timer и корректно управляйте отменой.

«Периодические задачи и таймеры» — бесплатный урок C# Academy на CoddyKit. Это урок 3 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения C# Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс C# Academy содержит 4 уроков всего.

Планирование повторяющихся задач

Многим фоновым службам требуется работать по фиксированному расписанию: каждые 5 минут, каждый час или ежедневно. .NET предоставляет несколько механизмов: PeriodicTimer (современный), System.Threading.Timer (классический) и Quartz.NET (на основе cron).

PeriodicTimer: современный выбор

PeriodicTimer (появившийся в .NET 6) предназначен для использования в методах async. Он срабатывает точно по расписанию, не использует обратные вызовы и хорошо интегрируется с отменой.

public class MetricsCollectorService : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken ct)
    {
        using var timer = new PeriodicTimer(TimeSpan.FromMinutes(1));

        while (await timer.WaitForNextTickAsync(ct))
        {
            // Runs every 60 seconds
            await CollectMetricsAsync(ct);
        }
        // Loop exits cleanly when ct is cancelled
    }
}

PeriodicTimer и цикл Task.Delay

PeriodicTimer лучше цикла Task.Delay: он срабатывает через фиксированные интервалы от времени начала (а не от времени начала плюс время обработки), предотвращая накопление отклонения за много итераций.

// Task.Delay loop — drifts! (interval = delay + work time)
while (!ct.IsCancellationRequested)
{
    await DoWorkAsync(ct); // takes 2 seconds
    await Task.Delay(5000, ct); // actual period: 7 seconds
}

// PeriodicTimer — no drift
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(5));
while (await timer.WaitForNextTickAsync(ct))
{
    await DoWorkAsync(ct); // period is still 5 seconds from start
}

System.Threading.Timer

System.Threading.Timer — это классический таймер пула потоков, использующий обратный вызов. Он поддерживает периодический и однократный режимы. Будьте внимательны: обратный вызов выполняется в потоке пула без поддержки async.

public class LegacyTimerService : BackgroundService, IDisposable
{
    private Timer? _timer;

    protected override Task ExecuteAsync(CancellationToken ct)
    {
        // Fire immediately then every 10 seconds
        _timer = new Timer(DoWork, null,
            dueTime:  TimeSpan.Zero,
            period:   TimeSpan.FromSeconds(10));
        return Task.CompletedTask;
    }

    private void DoWork(object? state)
    {
        // Runs on thread pool — avoid async void here
        Console.WriteLine($"Timer fired: {DateTime.UtcNow}");
    }

    public override void Dispose() { _timer?.Dispose(); base.Dispose(); }
}

Однократное выполнение с задержкой

Для однократного выполнения с задержкой используйте PeriodicTimer с одним срабатыванием либо сочетайте CancellationTokenSource.CancelAfter с Task.Delay.

// Run once after a delay
public async Task ScheduleOnceAsync(TimeSpan delay, Func<Task> action, CancellationToken ct)
{
    await Task.Delay(delay, ct);
    await action();
}

// Or with CancellationToken:
var cts = new CancellationTokenSource(TimeSpan.FromHours(1));
try
{
    await Task.Delay(Timeout.Infinite, cts.Token);
}
catch (OperationCanceledException)
{
    await RunScheduledJobAsync();
}

PeriodicTimer с обработкой ошибок

Всегда оборачивайте работу на каждом срабатывании в try/catch, чтобы единичный сбой не остановил дальнейшие срабатывания таймера.

protected override async Task ExecuteAsync(CancellationToken ct)
{
    using var timer = new PeriodicTimer(TimeSpan.FromMinutes(5));

    while (await timer.WaitForNextTickAsync(ct))
    {
        try
        {
            await SyncInventoryAsync(ct);
        }
        catch (Exception ex)
        {
            // Log but don't crash — next tick will retry
            _logger.LogError(ex, "Inventory sync failed");
        }
    }
}

Синхронизация с границами времени

Чтобы запускать задачу в определённое время (например, каждый час ровно в начале часа), перед запуском таймера вычислите начальную задержку до следующей временной границы.

protected override async Task ExecuteAsync(CancellationToken ct)
{
    // Wait until the next full hour
    var now = DateTime.UtcNow;
    var nextHour = now.Date.AddHours(now.Hour + 1);
    var initialDelay = nextHour - now;

    await Task.Delay(initialDelay, ct);

    // Then tick every hour exactly
    using var timer = new PeriodicTimer(TimeSpan.FromHours(1));

    await RunJobAsync(ct); // run at the first tick
    while (await timer.WaitForNextTickAsync(ct))
        await RunJobAsync(ct);
}

Защита от параллельных срабатываний

Если работа может занимать больше одного периода таймера, защищайтесь от параллельного выполнения с помощью SemaphoreSlim или пропускайте срабатывание, если предыдущая задача ещё выполняется.

private readonly SemaphoreSlim _lock = new(1, 1);

protected override async Task ExecuteAsync(CancellationToken ct)
{
    using var timer = new PeriodicTimer(TimeSpan.FromSeconds(30));

    while (await timer.WaitForNextTickAsync(ct))
    {
        // Skip if previous tick is still running
        if (!await _lock.WaitAsync(0, ct))
        {
            _logger.LogWarning("Skipping tick — previous still running");
            continue;
        }
        try { await DoWorkAsync(ct); }
        finally { _lock.Release(); }
    }
}

Jitter для распределённых служб

Когда несколько экземпляров службы запускаются одновременно, добавление случайного jitter к начальной задержке предотвращает лавинообразную нагрузку на общие ресурсы.

protected override async Task ExecuteAsync(CancellationToken ct)
{
    // Add jitter: 0-5 seconds random delay on startup
    var jitter = TimeSpan.FromSeconds(Random.Shared.NextDouble() * 5);
    await Task.Delay(jitter, ct);

    using var timer = new PeriodicTimer(TimeSpan.FromMinutes(1));
    while (await timer.WaitForNextTickAsync(ct))
        await RunJobAsync(ct);
}

Практический пример: служба обновления кэша

Периодическая служба, обновляющая распределённый кэш каждые 10 минут с использованием jitter и восстановлением после ошибок.

public class CacheRefreshService : BackgroundService
{
    private readonly IServiceScopeFactory _factory;
    private readonly ILogger<CacheRefreshService> _log;

    protected override async Task ExecuteAsync(CancellationToken ct)
    {
        // Spread startups across instances
        await Task.Delay(TimeSpan.FromSeconds(Random.Shared.Next(0, 30)), ct);

        using var timer = new PeriodicTimer(TimeSpan.FromMinutes(10));
        while (await timer.WaitForNextTickAsync(ct))
        {
            try
            {
                using var scope = _factory.CreateScope();
                var cache = scope.ServiceProvider.GetRequiredService<ICacheService>();
                await cache.RefreshAllAsync(ct);
                _log.LogInformation("Cache refreshed at {Time}", DateTime.UtcNow);
            }
            catch (Exception ex) { _log.LogError(ex, "Cache refresh failed"); }
        }
    }
}

Быстрая проверка

Почему для повторяющихся задач предпочтительнее PeriodicTimer, а не цикл Task.Delay?

Итоги: периодические задачи и таймеры

Главные выводы:

  • PeriodicTimer: поддержка async и отмены, отсутствие накопления отклонения — современный выбор
  • System.Threading.Timer: на основе обратного вызова, хорошо подходит для интеграции с устаревшим кодом
  • Оборачивайте работу на каждом срабатывании в try/catch, чтобы ошибки не останавлили таймер
  • Добавляйте jitter для распределённых экземпляров, чтобы избежать лавинообразной нагрузки
  • Используйте SemaphoreSlim, чтобы пропустить срабатывание, если предыдущее ещё выполняется
  • Вычисляйте начальную задержку, чтобы синхронизировать срабатывания с границами времени

Часто задаваемые вопросы

Урок «Периодические задачи и таймеры» бесплатный?

Да — полный текст урока «Периодические задачи и таймеры» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс C# Academy, подпишись на CoddyKit PRO. Курс C# Academy содержит 4 уроков всего.

Чему я научусь в уроке «Периодические задачи и таймеры»?

Планируйте повторяющиеся задачи с помощью PeriodicTimer и System.Threading.Timer и корректно управляйте отменой. Ты практикуешь C# Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать C# Academy?

Предыдущий опыт не требуется. C# Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 3 из 4.

Сколько времени занимает урок «Периодические задачи и таймеры»?

Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.

Можно ли писать и запускать код в этом уроке C# Academy?

Да. Каждый урок C# Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

Все уроки этого курса

  1. IHostedService и BackgroundService
  2. Проекты рабочих служб
  3. Периодические задачи и таймеры
  4. Плановые задания Quartz.NET
← Назад к C# Academy