0Pricing
C# Academy · Lección

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 moderna
  • System.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 SemaphoreSlim para 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

  1. IHostedService y BackgroundService
  2. Proyectos Worker Service
  3. Tareas periódicas y temporizadores
  4. Trabajos programados con Quartz.NET
← Volver a C# Academy