Menu

Timer em C#: System.Threading.Timer, System.Timers.Timer, PeriodicTimer

C# tem várias classes de timer, e elas se comportam de forma diferente. Veja quando usar System.Threading.Timer, System.Timers.Timer, PeriodicTimer ou um timer de interface, como iniciá-los e pará-los, por que os callbacks executam no thread pool e podem se sobrepor, por que um timer precisa continuar referenciado e como medir o tempo decorrido com Stopwatch.

Esta página tem editores executáveis - edite, execute e veja a saída na hora.

Um timer executa código depois de um atraso ou em um intervalo que se repete: atualizar um cache a cada minuto, salvar um rascunho a cada 30 segundos, consultar um serviço, atualizar um relógio. O .NET tem várias classes de timer que parecem iguais, mas diferem em onde o callback executa e em como você as controla. Esta página cobre cada uma, e também o Stopwatch para medir quanto tempo algo levou.

Qual timer usar

TimerO callback executa emEstiloUse para
System.Threading.TimerThread poolDelegate de callbackTrabalho em segundo plano em serviços e bibliotecas
System.Timers.TimerThread pool (por padrão)Evento Elapsed, Start/StopCódigo no estilo de componentes que gosta de eventos
PeriodicTimer (.NET 6+)Onde o seu laço async executarawait WaitForNextTickAsync()Código async; os ticks nunca se sobrepõem
System.Windows.Forms.Timer, DispatcherTimer do WPFA thread de interfaceEvento TickAtualizar controles em apps desktop

Os dois primeiros disparam em threads do thread pool, então os callbacks podem executar ao mesmo tempo que o resto do programa, e até ao mesmo tempo uns que os outros. Os timers de interface executam na thread de interface, e é por isso que podem mexer diretamente nos controles, e por isso um handler lento congela a janela.

System.Threading.Timer

O construtor recebe o callback, um objeto de estado, o atraso antes do primeiro tick e o período. Ele começa imediatamente.

Exemplo de saída:

tick 1 at ~200 ms
tick 2 at ~500 ms
tick 3 at ~800 ms
tick 4 at ~1100 ms
stopped after 4 ticks

Dois detalhes importam aqui. O Main precisa esperar (done.WaitOne()); um programa de console termina quando o Main retorna, e os callbacks do timer executam em threads de segundo plano que não mantêm o processo vivo. E o timer fica em um campo static: um System.Threading.Timer que nada referencia pode ser coletado pelo garbage collector, e aí para de disparar sem aviso. Isso acontece com mais frequência quando um timer é criado como variável local dentro de um método que retorna.

Change(dueTime, period) reagenda o timer; Timeout.Infinite nos dois o pausa, e um novo par o reinicia. Dispose() o para de vez.

System.Timers.Timer

System.Timers.Timer envolve o mesmo mecanismo em uma API baseada em eventos: defina Interval, inscreva-se em Elapsed e depois chame Start() e Stop().

Exemplo de saída:

autosave #1
autosave #2
autosave #3
autosave stopped

AutoReset = false cria um timer de disparo único: ele dispara uma vez e para, e chamar Start() de novo agenda o próximo tick único. Enabled = true e false são o mesmo que Start() e Stop(). O nome completo System.Timers.Timer está escrito aqui porque System.Threading também tem um Timer; com os dois namespaces importados, um Timer sozinho é ambíguo e não compila.

System.Timers.Timer também tem uma propriedade SynchronizingObject que leva o Elapsed para uma thread de interface no WinForms. Na prática, o próprio timer do framework de interface é mais simples para isso.

Callbacks se sobrepõem e executam em outras threads

As duas classes acima disparam no thread pool, no horário, tenha ou não o callback anterior terminado. Se um callback leva 3 segundos e o período é de 1 segundo, três callbacks executam ao mesmo tempo. Isso gera duas obrigações:

  • Tudo o que o callback toca precisa ser thread safe. Use Interlocked para contadores (como os exemplos fazem) e um lock para qualquer coisa maior.
  • Proteja contra a sobreposição quando ela importar. Pule um tick enquanto o anterior ainda estiver executando, ou use um padrão que não pode se sobrepor.

Uma proteção de pulo com Interlocked:

private int running = 0;

private void OnTick(object state)
{
    if (Interlocked.Exchange(ref running, 1) == 1) return; // previous tick still busy
    try
    {
        SyncOrders();   // slow work
    }
    finally
    {
        Volatile.Write(ref running, 0);
    }
}

Exceções são a outra armadilha. Uma exceção que escapa de um callback de System.Threading.Timer derruba o processo, enquanto System.Timers.Timer engole as exceções dos handlers de Elapsed, e a falha passa despercebida. Envolva o corpo de um callback de timer em try/catch e registre o erro.

PeriodicTimer e laços async

Em código async, um laço que espera entre as iterações é o timer mais claro: a próxima espera só começa depois que o trabalho termina, então os ticks nunca se sobrepõem, e as exceções aparecem onde você pode capturá-las. O .NET 6 adicionou PeriodicTimer exatamente para isso:

using var timer = new PeriodicTimer(TimeSpan.FromSeconds(30));

while (await timer.WaitForNextTickAsync(cancellationToken))
{
    await SaveDraftAsync();
}

WaitForNextTickAsync retorna false depois que o timer é descartado, o que encerra o laço, e lança OperationCanceledException quando o token é cancelado. Ao contrário de um laço em volta de Task.Delay, um PeriodicTimer mantém um ritmo fixo: se o trabalho leva 2 segundos, o próximo tick ainda vem 30 segundos depois do tick anterior, não 32.

Em qualquer versão, a mesma forma com Task.Delay funciona quando o desvio não importa:

Exemplo de saída:

poll 1
poll 2
poll 3
poll 4
polling cancelled after 4 rounds

O cancelamento substitui o Stop(): passe um token para o laço e cancele-o de fora. Veja tasks para mais sobre tokens de cancelamento.

Medindo o tempo decorrido com Stopwatch

Um timer agenda código; Stopwatch mede quanto tempo o código levou. Ele lê um relógio de alta resolução que só anda para frente, o que o torna a ferramenta certa para cronometrar:

Exemplo de saída:

string +=      64 ms
StringBuilder  0 ms
Same length: True
High resolution clock: True

Start, Stop, Reset e Restart o controlam; Elapsed é um TimeSpan, e ElapsedMilliseconds e ElapsedTicks dão números brutos. Evite cronometrar com DateTime.Now: a resolução dele pode ser de 10 a 15 ms, e ele pula quando o relógio do sistema é ajustado. Para benchmarks sérios, em que o aquecimento do JIT e a coleta de lixo distorcem uma única execução, use a biblioteca BenchmarkDotNet.

Precisão dos timers

O intervalo de um timer é um mínimo, não uma promessa. No Windows, a resolução padrão do timer do sistema é de cerca de 15,6 ms, então um intervalo de 10 ms tende a disparar a cada 15 ou 16 ms, e em uma máquina ocupada um callback do thread pool pode começar ainda mais tarde. Timers servem para "mais ou menos a cada N segundos". Para temporização precisa de quadros ou de mídia, use APIs feitas para isso.

Erros comuns

  • Um System.Threading.Timer guardado só em uma variável local. Ele pode ser coletado pelo garbage collector e parar de disparar; guarde-o em um campo.
  • Deixar o Main retornar. O processo termina e leva o timer junto.
  • Timer ambíguo. Com System.Threading e System.Timers importados, escreva o nome completo.
  • Supor que os ticks nunca se sobrepõem. Timers do thread pool se sobrepõem quando o trabalho é mais lento que o intervalo.
  • Mexer em controles de interface a partir de um timer do thread pool. Use o timer do framework de interface ou volte para a thread de interface.
  • Exceções não tratadas nos callbacks. Elas derrubam o processo (Threading.Timer) ou somem (Timers.Timer); capture e registre.
  • Esquecer o Dispose. Timers seguram recursos do sistema e continuam disparando até serem descartados.

Perguntas frequentes

Qual timer devo usar em C#?

Em código async no .NET 6 ou posterior, PeriodicTimer com await timer.WaitForNextTickAsync() em um laço. Para um callback no thread pool, System.Threading.Timer. Para um timer baseado em eventos com Start, Stop e um evento Elapsed, System.Timers.Timer. Em WinForms ou WPF, use o timer do framework de interface para que o handler execute na thread de interface.

Como executar código a cada poucos segundos em C#?

Crie um timer com o intervalo, por exemplo new System.Threading.Timer(_ => Refresh(), null, 0, 5000) para chamar Refresh agora e a cada 5 segundos, e mantenha uma referência a ele. Em código async, faça um laço com PeriodicTimer (.NET 6+) ou await Task.Delay(5000); a versão com laço nunca executa dois ticks ao mesmo tempo.

Como parar um timer em C#?

Para System.Timers.Timer, chame Stop() (ou defina Enabled = false), e Dispose() quando não precisar mais dele. Para System.Threading.Timer, chame Change(Timeout.Infinite, Timeout.Infinite) para pausá-lo, ou Dispose() para pará-lo de vez. Um callback que já começou ainda pode terminar depois que você para o timer.

Por que meu System.Threading.Timer para de disparar?

Nada referencia o timer, então o garbage collector o coletou e os callbacks pararam. Isso acontece quando o timer é criado como variável local em um método que retorna. Guarde-o em um campo enquanto ele precisar continuar executando.

Como medir o tempo decorrido em C#?

Use System.Diagnostics.Stopwatch: var sw = Stopwatch.StartNew(); ... sw.Stop(); e depois leia sw.ElapsedMilliseconds ou sw.Elapsed. Ele usa um relógio monotônico de alta resolução, ao contrário de subtrair dois valores de DateTime.Now, que tem resolução mais grosseira e pula quando o relógio do sistema é ajustado.

Coddy programming languages illustration

Aprenda a programar com o Coddy

COMEÇAR