Периодические задачи и таймеры
Планируйте повторяющиеся задачи с помощью 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 — локальная установка не требуется.
Все уроки этого курса
- IHostedService и BackgroundService
- Проекты рабочих служб
- Периодические задачи и таймеры
- Плановые задания Quartz.NET