Un temporizador ejecuta código tras un retraso o con un intervalo que se repite: refrescar una caché cada minuto, guardar un borrador cada 30 segundos, consultar un servicio, actualizar un reloj. .NET tiene varias clases de temporizador que se parecen pero se diferencian en dónde se ejecuta el callback y en cómo se controlan. Esta página trata cada una, además de Stopwatch para medir cuánto tardó algo.
Qué temporizador usar
| Temporizador | El callback se ejecuta en | Estilo | Úsalo para |
|---|---|---|---|
System.Threading.Timer | El pool de hilos | Delegado callback | Trabajo en segundo plano en servicios y bibliotecas |
System.Timers.Timer | El pool de hilos (por defecto) | Evento Elapsed, Start/Stop | Código al estilo de componentes al que le gustan los eventos |
PeriodicTimer (.NET 6+) | Donde se ejecute tu bucle async | await WaitForNextTickAsync() | Código async; los ticks nunca se solapan |
System.Windows.Forms.Timer, DispatcherTimer de WPF | El hilo de la interfaz | Evento Tick | Actualizar controles en aplicaciones de escritorio |
Los dos primeros se disparan en hilos del pool, así que sus callbacks pueden ejecutarse a la vez que el resto del programa, e incluso a la vez entre ellos. Los temporizadores de interfaz se ejecutan en el hilo de la interfaz, y por eso pueden tocar los controles directamente, y por eso un manejador lento congela la ventana.
System.Threading.Timer
El constructor recibe el callback, un objeto de estado, el retraso antes del primer tick y el periodo. Empieza de inmediato.
Salida de ejemplo:
tick 1 at ~200 ms
tick 2 at ~500 ms
tick 3 at ~800 ms
tick 4 at ~1100 ms
stopped after 4 ticks
Aquí importan dos detalles. Main tiene que esperar (done.WaitOne()); un programa de consola termina cuando Main vuelve, y los callbacks del temporizador se ejecutan en hilos en segundo plano que no mantienen vivo el proceso. Y el temporizador vive en un campo static: un System.Threading.Timer al que nada hace referencia puede ser liberado por el recolector de basura, y entonces deja de dispararse en silencio. Esto ocurre sobre todo cuando se crea un temporizador como variable local dentro de un método que termina.
Change(dueTime, period) reprograma el temporizador; Timeout.Infinite en los dos lo pausa, y un par nuevo lo vuelve a poner en marcha. Dispose() lo detiene definitivamente.
System.Timers.Timer
System.Timers.Timer envuelve el mismo mecanismo en una API basada en eventos: asigna Interval, suscríbete a Elapsed y después llama a Start() y Stop().
Salida de ejemplo:
autosave #1
autosave #2
autosave #3
autosave stopped
AutoReset = false crea un temporizador de un solo disparo: se dispara una vez y se detiene, y volver a llamar a Start() programa el siguiente tick único. Enabled = true y false equivalen a Start() y Stop(). Aquí se escribe el nombre completo System.Timers.Timer porque System.Threading también tiene un Timer; con los dos namespaces importados, un Timer a secas es ambiguo y no compila.
System.Timers.Timer también tiene una propiedad SynchronizingObject que lleva Elapsed al hilo de la interfaz en WinForms. En la práctica, para eso es más sencillo el propio temporizador del framework de interfaz.
Los callbacks se solapan y se ejecutan en otros hilos
Las dos clases anteriores se disparan en el pool de hilos, según lo programado, haya terminado o no el callback anterior. Si un callback tarda 3 segundos y el periodo es de 1 segundo, se ejecutan tres callbacks a la vez. Eso crea dos obligaciones:
- Todo lo que toca el callback debe ser seguro para hilos. Usa
Interlockedpara los contadores (como hacen los ejemplos) y un lock para cualquier cosa más grande. - Protégete del solapamiento cuando importe. O bien sáltate un tick mientras el anterior siga ejecutándose, o bien usa un patrón que no pueda solaparse.
Una guarda para saltar ticks con 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);
}
}
Las excepciones son la otra trampa. Una excepción que escapa de un callback de System.Threading.Timer hace caer el proceso, mientras que System.Timers.Timer se traga las excepciones de los manejadores de Elapsed, así que el fallo pasa desapercibido. Envuelve el cuerpo de un callback de temporizador en un try/catch y registra el error.
PeriodicTimer y bucles async
En código async, un bucle que espera entre iteraciones es el temporizador más claro: la siguiente espera empieza solo después de que termine el trabajo, así que los ticks nunca pueden solaparse, y las excepciones aparecen donde puedes capturarlas. .NET 6 añadió PeriodicTimer justo para esto:
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(30));
while (await timer.WaitForNextTickAsync(cancellationToken))
{
await SaveDraftAsync();
}
WaitForNextTickAsync devuelve false una vez liberado el temporizador, lo que termina el bucle, y lanza OperationCanceledException cuando se cancela el token. A diferencia de un bucle alrededor de Task.Delay, un PeriodicTimer mantiene un ritmo fijo: si el trabajo tarda 2 segundos, el siguiente tick sigue llegando 30 segundos después del anterior, no 32.
En cualquier versión, la misma forma con Task.Delay funciona cuando la deriva no importa:
Salida de ejemplo:
poll 1
poll 2
poll 3
poll 4
polling cancelled after 4 rounds
La cancelación sustituye a Stop(): pasa un token al bucle y cancélalo desde fuera. Consulta tareas para saber más sobre los tokens de cancelación.
Medir el tiempo transcurrido con Stopwatch
Un temporizador programa código; Stopwatch mide cuánto tardó el código. Lee un reloj de alta resolución que solo avanza, lo que lo convierte en la herramienta correcta para medir tiempos:
Salida de ejemplo:
string += 64 ms
StringBuilder 0 ms
Same length: True
High resolution clock: True
Start, Stop, Reset y Restart lo controlan; Elapsed es un TimeSpan, y ElapsedMilliseconds y ElapsedTicks dan números en bruto. Evita medir tiempos con DateTime.Now: su resolución puede ser tan gruesa como de 10 a 15 ms, y salta cuando se ajusta el reloj del sistema. Para benchmarks serios, donde el calentamiento del JIT y la recolección de basura distorsionan una sola ejecución, usa la biblioteca BenchmarkDotNet.
Precisión de los temporizadores
El intervalo de un temporizador es un mínimo, no una promesa. En Windows, la resolución por defecto del temporizador del sistema es de unos 15,6 ms, así que un intervalo de 10 ms tiende a dispararse cada 15 o 16 ms, y en una máquina ocupada un callback del pool de hilos puede empezar aún más tarde. Los temporizadores sirven para "más o menos cada N segundos". Para una sincronización precisa de fotogramas o de multimedia, usa API diseñadas para eso.
Errores comunes
- Un
System.Threading.Timerguardado solo en una variable local. Puede ser liberado por el recolector de basura y dejar de dispararse; guárdalo en un campo. - Dejar que
Mainvuelva. El proceso termina y se lleva el temporizador con él. Timerambiguo. ConSystem.ThreadingySystem.Timersimportados a la vez, escribe el nombre completo.- Suponer que los ticks nunca se solapan. Los temporizadores del pool de hilos se solapan cuando el trabajo es más lento que el intervalo.
- Tocar controles de la interfaz desde un temporizador del pool de hilos. Usa el temporizador del framework de interfaz o vuelve al hilo de la interfaz.
- Excepciones no manejadas en los callbacks. Hacen caer el proceso (
Threading.Timer) o desaparecen (Timers.Timer); captúralas y regístralas. - Olvidar
Dispose. Los temporizadores ocupan recursos del sistema y siguen disparándose hasta que se liberan.
Preguntas frecuentes
¿Qué temporizador debo usar en C#?
En código async sobre .NET 6 o posterior, PeriodicTimer con await timer.WaitForNextTickAsync() dentro de un bucle. Para un callback en el pool de hilos, System.Threading.Timer. Para un temporizador basado en eventos con Start, Stop y un evento Elapsed, System.Timers.Timer. En WinForms o WPF, usa el temporizador del framework de interfaz para que el manejador se ejecute en el hilo de la interfaz.
¿Cómo ejecuto código cada pocos segundos en C#?
Crea un temporizador con el intervalo, por ejemplo new System.Threading.Timer(_ => Refresh(), null, 0, 5000) para llamar a Refresh ahora y cada 5 segundos, y guarda una referencia a él. En código async, haz un bucle con PeriodicTimer (.NET 6+) o con await Task.Delay(5000); la versión con bucle nunca ejecuta dos ticks a la vez.
¿Cómo detengo un temporizador en C#?
Con System.Timers.Timer, llama a Stop() (o pon Enabled = false), y a Dispose() cuando ya no lo necesites. Con System.Threading.Timer, llama a Change(Timeout.Infinite, Timeout.Infinite) para pausarlo, o a Dispose() para detenerlo definitivamente. Un callback que ya empezó puede terminar después de que detengas el temporizador.
¿Por qué mi System.Threading.Timer deja de dispararse?
Nada hace referencia al temporizador, así que el recolector de basura lo liberó y sus callbacks se detuvieron. Pasa cuando el temporizador se crea como variable local en un método que termina. Guárdalo en un campo mientras deba seguir funcionando.
¿Cómo mido el tiempo transcurrido en C#?
Usa System.Diagnostics.Stopwatch: var sw = Stopwatch.StartNew(); ... sw.Stop(); y después lee sw.ElapsedMilliseconds o sw.Elapsed. Usa un reloj monotónico de alta resolución, a diferencia de restar dos valores de DateTime.Now, que tiene una resolución más gruesa y salta cuando se ajusta el reloj del sistema.