Un timer esegue del codice dopo un ritardo o a intervalli regolari: aggiornare una cache ogni minuto, salvare una bozza ogni 30 secondi, interrogare un servizio, aggiornare un orologio. .NET ha diverse classi timer che si somigliano ma differiscono per il punto in cui gira la callback e per come le controlli. Questa pagina le copre tutte, più Stopwatch per misurare quanto tempo ha richiesto qualcosa.
Quale timer usare
| Timer | La callback gira su | Stile | Da usare per |
|---|---|---|---|
System.Threading.Timer | Thread pool | Delegato di callback | Lavoro in background in servizi e librerie |
System.Timers.Timer | Thread pool (per impostazione predefinita) | Evento Elapsed, Start/Stop | Codice a componenti che preferisce gli eventi |
PeriodicTimer (.NET 6+) | Ovunque giri il tuo ciclo asincrono | await WaitForNextTickAsync() | Codice asincrono; i tick non si sovrappongono mai |
System.Windows.Forms.Timer, DispatcherTimer di WPF | Il thread dell'interfaccia | Evento Tick | Aggiornare i controlli nelle app desktop |
I primi due scattano su thread del thread pool, quindi le loro callback possono girare nello stesso momento del resto del programma, e perfino nello stesso momento l'una dell'altra. I timer dell'interfaccia girano sul thread dell'interfaccia, ed è per questo che possono toccare direttamente i controlli, e anche per questo un gestore lento blocca la finestra.
System.Threading.Timer
Il costruttore accetta la callback, un oggetto di stato, il ritardo prima del primo tick e il periodo. Parte subito.
Output di esempio:
tick 1 at ~200 ms
tick 2 at ~500 ms
tick 3 at ~800 ms
tick 4 at ~1100 ms
stopped after 4 ticks
Qui contano due dettagli. Main deve aspettare (done.WaitOne()); un programma console termina quando Main ritorna, e le callback dei timer girano su thread in background che non tengono in vita il processo. E il timer vive in un campo statico: un System.Threading.Timer a cui nulla fa riferimento può essere raccolto dal garbage collector, e a quel punto smette di scattare senza avvisare. Capita soprattutto quando un timer viene creato come variabile locale dentro un metodo che poi ritorna.
Change(dueTime, period) ripianifica il timer; Timeout.Infinite per entrambi lo mette in pausa, e una nuova coppia di valori lo fa ripartire. Dispose() lo ferma definitivamente.
System.Timers.Timer
System.Timers.Timer avvolge lo stesso meccanismo in un'API basata su eventi: imposta Interval, iscriviti a Elapsed, poi Start() e Stop().
Output di esempio:
autosave #1
autosave #2
autosave #3
autosave stopped
AutoReset = false crea un timer a colpo singolo: scatta una volta e si ferma, e chiamare di nuovo Start() pianifica il tick singolo successivo. Enabled = true e false equivalgono a Start() e Stop(). Qui si scrive il nome completo System.Timers.Timer perché anche System.Threading ha un Timer; con entrambi i namespace importati, un Timer senza qualificazione è ambiguo e non compila.
System.Timers.Timer ha anche una proprietà SynchronizingObject che trasferisce Elapsed sul thread dell'interfaccia in WinForms. In pratica, per questo è più semplice il timer del framework dell'interfaccia.
Le callback si sovrappongono e girano su altri thread
Entrambe le classi viste sopra scattano sul thread pool, secondo il programma, che la callback precedente abbia finito o no. Se una callback impiega 3 secondi e il periodo è di 1 secondo, tre callback girano insieme. Questo crea due obblighi:
- Tutto ciò che la callback tocca deve essere thread-safe. Usa
Interlockedper i contatori (come fanno gli esempi) e un lock per qualsiasi cosa più grande. - Proteggiti dalla sovrapposizione quando conta. Salta un tick mentre quello precedente sta ancora girando, oppure usa uno schema che non può sovrapporsi.
Una protezione che salta i tick 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);
}
}
Le eccezioni sono l'altra trappola. Un'eccezione che sfugge a una callback di System.Threading.Timer fa crashare il processo, mentre System.Timers.Timer inghiotte le eccezioni dei gestori di Elapsed, così l'errore passa inosservato. Racchiudi il corpo di una callback del timer in try/catch e registra l'errore nel log.
PeriodicTimer e cicli asincroni
Nel codice asincrono, un ciclo che aspetta tra un'iterazione e l'altra è il timer più chiaro: l'attesa successiva inizia solo dopo che il lavoro è finito, quindi i tick non possono mai sovrapporsi, e le eccezioni emergono dove puoi catturarle. .NET 6 ha aggiunto PeriodicTimer proprio per questo:
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(30));
while (await timer.WaitForNextTickAsync(cancellationToken))
{
await SaveDraftAsync();
}
WaitForNextTickAsync restituisce false dopo che il timer è stato eliminato con Dispose, il che termina il ciclo, e lancia OperationCanceledException quando il token viene cancellato. A differenza di un ciclo attorno a Task.Delay, un PeriodicTimer mantiene un ritmo fisso: se il lavoro richiede 2 secondi, il tick successivo arriva comunque 30 secondi dopo il precedente, non 32.
In qualsiasi versione, la stessa struttura con Task.Delay funziona quando lo slittamento non conta:
Output di esempio:
poll 1
poll 2
poll 3
poll 4
polling cancelled after 4 rounds
La cancellazione sostituisce Stop(): passa un token al ciclo e cancellalo dall'esterno. Vedi task per saperne di più sui token di cancellazione.
Misurare il tempo trascorso con Stopwatch
Un timer pianifica il codice; Stopwatch misura quanto tempo ha impiegato il codice. Legge un orologio ad alta risoluzione che va solo avanti, e per questo è lo strumento giusto per cronometrare:
Output di esempio:
string += 64 ms
StringBuilder 0 ms
Same length: True
High resolution clock: True
Lo controlli con Start, Stop, Reset e Restart; Elapsed è un TimeSpan, e ElapsedMilliseconds ed ElapsedTicks danno i numeri grezzi. Evita di cronometrare con DateTime.Now: la sua risoluzione può arrivare a 10 o 15 ms, e fa salti quando l'orologio di sistema viene regolato. Per benchmark seri, dove il riscaldamento del JIT e la garbage collection falsano una singola esecuzione, usa la libreria BenchmarkDotNet.
Precisione dei timer
L'intervallo di un timer è un minimo, non una promessa. Su Windows la risoluzione predefinita del timer di sistema è di circa 15,6 ms, quindi un intervallo di 10 ms tende a scattare ogni 15 o 16 ms, e su una macchina carica una callback del thread pool può partire ancora più tardi. I timer vanno bene per "circa ogni N secondi". Per temporizzazioni precise di fotogrammi o contenuti multimediali, usa le API pensate per quello.
Errori comuni
- Un
System.Threading.Timermemorizzato solo in una variabile locale. Può essere raccolto dal garbage collector e smettere di scattare; tienilo in un campo. - Lasciare che
Mainritorni. Il processo termina e si porta via il timer. Timerambiguo. ConSystem.ThreadingeSystem.Timersentrambi importati, scrivi il nome completo.- Dare per scontato che i tick non si sovrappongano mai. I timer del thread pool si sovrappongono quando il lavoro è più lento dell'intervallo.
- Toccare i controlli dell'interfaccia da un timer del thread pool. Usa il timer del framework dell'interfaccia o riporta l'esecuzione sul thread dell'interfaccia.
- Eccezioni non gestite nelle callback. Fanno crashare il processo (
Threading.Timer) o spariscono (Timers.Timer); catturale e registrale. - Dimenticare
Dispose. I timer occupano risorse di sistema e continuano a scattare finché non vengono eliminati.
Domande frequenti
Quale timer dovrei usare in C#?
Nel codice asincrono su .NET 6 o successivi, PeriodicTimer con await timer.WaitForNextTickAsync() in un ciclo. Per una callback sul thread pool, System.Threading.Timer. Per un timer basato su eventi con Start, Stop e un evento Elapsed, System.Timers.Timer. In WinForms o WPF, usa il timer del framework dell'interfaccia, così il gestore gira sul thread dell'interfaccia.
Come eseguo del codice ogni pochi secondi in C#?
Crea un timer con l'intervallo, per esempio new System.Threading.Timer(_ => Refresh(), null, 0, 5000) per chiamare Refresh subito e poi ogni 5 secondi, e tienine un riferimento. Nel codice asincrono, usa un ciclo con PeriodicTimer (.NET 6+) o await Task.Delay(5000); la versione a ciclo non esegue mai due tick contemporaneamente.
Come fermo un timer in C#?
Per System.Timers.Timer, chiama Stop() (o imposta Enabled = false), e Dispose() quando non ti serve più. Per System.Threading.Timer, chiama Change(Timeout.Infinite, Timeout.Infinite) per metterlo in pausa, o Dispose() per fermarlo definitivamente. Una callback già partita può comunque terminare dopo che hai fermato il timer.
Perché il mio System.Threading.Timer smette di scattare?
Nulla fa riferimento al timer, quindi il garbage collector lo ha raccolto e le sue callback si sono fermate. Succede quando il timer viene creato come variabile locale in un metodo che poi ritorna. Salvalo in un campo per tutto il tempo in cui deve continuare a girare.
Come misuro il tempo trascorso in C#?
Usa System.Diagnostics.Stopwatch: var sw = Stopwatch.StartNew(); ... sw.Stop(); poi leggi sw.ElapsedMilliseconds o sw.Elapsed. Usa un orologio monotono ad alta risoluzione, a differenza della sottrazione di due valori DateTime.Now, che ha una risoluzione più grossolana e fa salti quando l'orologio di sistema viene regolato.