0Pricing
C# Academy · Aula

Tarefas periódicas e temporizadores

Agende trabalho recorrente com PeriodicTimer e System.Threading.Timer e gerencie o cancelamento com elegância.

Tarefas periódicas e temporizadores é uma aula grátis de C# Academy no CoddyKit. Esta é a aula 3 de 4. Você pode ler a aula completa abaixo gratuitamente — depois pratica ao vivo no navegador com um editor de código integrado e um tutor de IA 24/7. Faz parte do caminho de aprendizado de C# Academy, e seu progresso é sincronizado entre a web e o app CoddyKit. O curso de C# Academy inclui 4 aulas no total.

Agendamento de trabalho recorrente

Muitos serviços em segundo plano precisam ser executados em uma agenda fixa — a cada 5 minutos, a cada hora ou diariamente. O .NET fornece vários mecanismos: PeriodicTimer (moderno), System.Threading.Timer (clássico) e o Quartz.NET (baseado em cron).

PeriodicTimer: a opção moderna

PeriodicTimer (introduzido no .NET 6) foi projetado para uso em métodos async. Ele dispara exatamente conforme a agenda, sem retornos de chamada, e integra-se de forma limpa ao cancelamento.

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 versus loop com Task.Delay

PeriodicTimer é melhor que um loop com Task.Delay: ele dispara em intervalos fixos a partir da hora de início (não da hora de início + tempo de processamento), evitando desvios ao longo de muitas iterações.

// 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 é um temporizador clássico do conjunto de linhas de execução que usa um retorno de chamada. Ele oferece suporte aos modos periódico e de execução única. Atenção: o retorno de chamada é executado em uma linha de execução do conjunto, sem suporte a 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(); }
}

Execução atrasada de disparo único

Use PeriodicTimer com um único disparo ou combine CancellationTokenSource.CancelAfter com Task.Delay para executar uma operação atrasada de disparo único.

// 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 com tratamento de erros

Envolva sempre o trabalho de cada disparo em try/catch para que uma única falha não impeça o temporizador de disparar novamente no próximo 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");
        }
    }
}

Alinhamento com limites do relógio

Para executar em horários específicos (por exemplo, a cada hora em ponto), calcule o atraso inicial até o próximo limite antes de iniciar o 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);
}

Proteção contra disparos simultâneos

Se o trabalho puder demorar mais que um período do temporizador, proteja contra a execução simultânea com um SemaphoreSlim ou ignore o disparo se o trabalho ainda estiver em execução.

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 serviços distribuídos

Quando várias instâncias de um serviço são iniciadas simultaneamente, adicionar um jitter aleatório ao atraso inicial evita problemas de efeito manada nos recursos compartilhados.

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);
}

Exemplo real: serviço de atualização de cache

Um serviço periódico que atualiza um cache distribuído a cada 10 minutos, com jitter e recuperação de erros.

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"); }
        }
    }
}

Verificação rápida

Por que PeriodicTimer é preferível a um loop com Task.Delay para tarefas recorrentes?

Recapitulação: tarefas periódicas e temporizadores

Principais conclusões:

  • PeriodicTimer: assíncrono, ciente do cancelamento, sem desvios — a opção moderna
  • System.Threading.Timer: baseado em retorno de chamada, adequado para integração com código legado
  • Envolva o trabalho de cada disparo em try/catch para que os erros não interrompam o temporizador
  • Adicione jitter para instâncias distribuídas a fim de evitar o efeito manada
  • Use SemaphoreSlim para ignorar um disparo se o anterior ainda estiver em execução
  • Calcule o atraso inicial para alinhar os disparos aos limites do relógio

Perguntas Frequentes

A aula “Tarefas periódicas e temporizadores” é grátis?

Sim — o texto completo de “Tarefas periódicas e temporizadores” é grátis para ler aqui na web. Para praticá-la interativamente (um editor de código integrado e um tutor de IA 24/7) e desbloquear o restante do curso de C# Academy, atualize para CoddyKit PRO. O curso de C# Academy inclui 4 aulas no total.

O que vou aprender em “Tarefas periódicas e temporizadores”?

Agende trabalho recorrente com PeriodicTimer e System.Threading.Timer e gerencie o cancelamento com elegância. Você pratica C# Academy com código prático que executa diretamente no navegador, e um tutor de IA 24/7 responde suas dúvidas enquanto trabalha na aula.

Preciso ter experiência prévia para começar C# Academy?

Nenhuma experiência prévia é necessária. C# Academy no CoddyKit é estruturado para alunos iniciantes até avançados, então você pode começar aqui ou desde o início e aprender no seu ritmo. Esta é a aula 3 de 4.

Quanto tempo leva a aula “Tarefas periódicas e temporizadores”?

A maioria das aulas CoddyKit leva cerca de 5–10 minutos. Cada uma é compacta e interativa, então você faz progresso constante e retoma exatamente de onde parou entre web e app.

Posso escrever e executar código nesta aula de C# Academy?

Sim. Cada aula de C# Academy inclui um editor de código integrado, então você escreve e executa código real direto no navegador e recebe feedback de IA instantaneamente — nenhuma configuração local necessária.

Todas as aulas deste curso

  1. IHostedService e BackgroundService
  2. Projetos de Worker Service
  3. Tarefas periódicas e temporizadores
  4. Trabalhos agendados com Quartz.NET
← Voltar para C# Academy