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
| Timer | O callback executa em | Estilo | Use para |
|---|---|---|---|
System.Threading.Timer | Thread pool | Delegate de callback | Trabalho em segundo plano em serviços e bibliotecas |
System.Timers.Timer | Thread pool (por padrão) | Evento Elapsed, Start/Stop | Código no estilo de componentes que gosta de eventos |
PeriodicTimer (.NET 6+) | Onde o seu laço async executar | await WaitForNextTickAsync() | Código async; os ticks nunca se sobrepõem |
System.Windows.Forms.Timer, DispatcherTimer do WPF | A thread de interface | Evento Tick | Atualizar 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
Interlockedpara 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.Timerguardado só em uma variável local. Ele pode ser coletado pelo garbage collector e parar de disparar; guarde-o em um campo. - Deixar o
Mainretornar. O processo termina e leva o timer junto. Timerambíguo. ComSystem.ThreadingeSystem.Timersimportados, 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.