Tareas periódicas y temporizadores
Programe trabajo recurrente con PeriodicTimer y System.Threading.Timer, y gestione la cancelación correctamente.
Tareas periódicas y temporizadores es una lección gratuita de C# Academy en CoddyKit. Esta es la lección 3 de 4. Puedes leer la lección completa abajo gratuitamente — luego la practicas en el navegador con un editor de código integrado y un tutor de IA 24/7. Forma parte de la ruta de aprendizaje de C# Academy, y tu progreso se sincroniza en la web y la app de CoddyKit. El curso de C# Academy incluye 4 lecciones en total.
Programación de trabajo recurrente
Muchos servicios en segundo plano deben ejecutarse según un horario fijo: cada 5 minutos, cada hora o diariamente. .NET proporciona varios mecanismos: PeriodicTimer (moderno), System.Threading.Timer (clásico) y Quartz.NET (basado en cron).
PeriodicTimer: la opción moderna
PeriodicTimer (introducido en .NET 6) está diseñado para utilizarse en métodos async. Se activa exactamente según el horario, sin callbacks, y se integra correctamente con la cancelación.
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 frente a un bucle con Task.Delay
PeriodicTimer es mejor que un bucle con Task.Delay: se activa en intervalos fijos desde la hora de inicio (no desde el inicio más el tiempo de procesamiento), lo que evita la desviación tras muchas iteraciones.
// 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 es un temporizador clásico del grupo de subprocesos que utiliza un callback. Admite modos periódico y de ejecución única. Tenga cuidado: el callback se ejecuta en un subproceso del grupo de subprocesos sin compatibilidad con 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(); }
}Ejecución diferida de una sola vez
Utilice PeriodicTimer con un único tick, o combine CancellationTokenSource.CancelAfter con Task.Delay, para realizar una ejecución diferida de una sola vez.
// 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 con manejo de errores
Encierre siempre el trabajo de cada tick en un bloque try/catch para que un solo fallo no impida que el temporizador vuelva a activarse en el siguiente ciclo.
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");
}
}
}Alineación con los límites del reloj
Para ejecutarse a horas concretas (por ejemplo, cada hora en punto), calcule el retraso inicial hasta el siguiente límite antes de iniciar el temporizador.
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);
}Protección frente a ticks simultáneos
Si el trabajo puede tardar más que un período del temporizador, protéjalo frente a la ejecución simultánea mediante un SemaphoreSlim o omita el tick si el trabajo aún está en ejecución.
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 para servicios distribuidos
Cuando varias instancias de un servicio se inician simultáneamente, añadir un jitter aleatorio al retraso inicial evita problemas de avalancha sobre los recursos compartidos.
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);
}Caso real: servicio de actualización de caché
Un servicio periódico que actualiza una caché distribuida cada 10 minutos, con jitter y recuperación ante errores.
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"); }
}
}
}Comprobación rápida
¿Por qué se prefiere PeriodicTimer a un bucle con Task.Delay para las tareas recurrentes?
Resumen: tareas periódicas y temporizadores
Aspectos clave:
PeriodicTimer: async, compatible con la cancelación y sin desviación; la opción modernaSystem.Threading.Timer: basado en callbacks, adecuado para integrarse con código heredado- Encierre el trabajo de cada tick en un bloque try/catch para que los errores no detengan el temporizador
- Añada jitter para evitar avalanchas entre instancias distribuidas
- Utilice
SemaphoreSlimpara omitir un tick si el anterior aún está en ejecución - Calcule el retraso inicial para alinear los ticks con los límites del reloj
Preguntas frecuentes
¿La lección «Tareas periódicas y temporizadores» es gratis?
Sí — el texto completo de «Tareas periódicas y temporizadores» es gratis para leer aquí en la web. Para practicarla de forma interactiva (editor de código integrado y tutor de IA 24/7) y desbloquear el resto del curso de C# Academy, actualiza a CoddyKit PRO. El curso de C# Academy incluye 4 lecciones en total.
¿Qué aprenderé en «Tareas periódicas y temporizadores»?
Programe trabajo recurrente con PeriodicTimer y System.Threading.Timer, y gestione la cancelación correctamente. Practicas C# Academy con código real que ejecutas directamente en el navegador, y un tutor de IA 24/7 responde tus preguntas mientras trabajas en la lección.
¿Necesito experiencia previa para empezar C# Academy?
No se requiere experiencia previa. C# Academy en CoddyKit está estructurado para principiantes hasta estudiantes avanzados, así que puedes empezar aquí o desde el inicio y avanzar a tu ritmo. Esta es la lección 3 de 4.
¿Cuánto tiempo toma la lección «Tareas periódicas y temporizadores»?
La mayoría de las lecciones de CoddyKit toman alrededor de 5–10 minutos. Cada una es compacta e interactiva, así que avanzas constantemente y retomas exactamente por donde dejaste en la web y la app.
¿Puedo escribir y ejecutar código en esta lección de C# Academy?
Sí. Cada lección de C# Academy incluye un editor de código integrado, así que escribes y ejecutas código real directamente en tu navegador y obtienes retroalimentación instantánea de IA — sin configuración local necesaria.
Todas las lecciones de este curso
- IHostedService y BackgroundService
- Proyectos Worker Service
- Tareas periódicas y temporizadores
- Trabajos programados con Quartz.NET