Un timer exécute du code après un délai ou à intervalle régulier : rafraîchir un cache chaque minute, enregistrer un brouillon toutes les 30 secondes, interroger un service, mettre à jour une horloge. .NET a plusieurs classes de timer qui se ressemblent mais diffèrent par l'endroit où s'exécute le callback et par la façon de les contrôler. Cette page les présente toutes, plus Stopwatch pour mesurer la durée de quelque chose.
Quel timer utiliser
| Timer | Le callback s'exécute sur | Style | Usage |
|---|---|---|---|
System.Threading.Timer | Pool de threads | Délégué de callback | Travail en arrière-plan dans les services et bibliothèques |
System.Timers.Timer | Pool de threads (par défaut) | Événement Elapsed, Start/Stop | Code de style composant qui aime les événements |
PeriodicTimer (.NET 6+) | Là où s'exécute votre boucle async | await WaitForNextTickAsync() | Code async ; les ticks ne se chevauchent jamais |
System.Windows.Forms.Timer, DispatcherTimer de WPF | Le thread d'interface | Événement Tick | Mettre à jour des contrôles dans les applications de bureau |
Les deux premiers se déclenchent sur des threads du pool, donc leurs callbacks peuvent s'exécuter en même temps que le reste de votre programme, et même en même temps les uns que les autres. Les timers d'interface s'exécutent sur le thread d'interface, c'est pourquoi ils peuvent toucher directement aux contrôles, et pourquoi un gestionnaire lent fige la fenêtre.
System.Threading.Timer
Le constructeur prend le callback, un objet d'état, le délai avant le premier tick et la période. Le timer démarre immédiatement.
Exemple de sortie :
tick 1 at ~200 ms
tick 2 at ~500 ms
tick 3 at ~800 ms
tick 4 at ~1100 ms
stopped after 4 ticks
Deux détails comptent ici. Main doit attendre (done.WaitOne()) ; un programme console se termine au retour de Main, et les callbacks du timer s'exécutent sur des threads d'arrière-plan qui ne maintiennent pas le processus en vie. Et le timer vit dans un champ statique : un System.Threading.Timer que rien ne référence peut être récupéré par le ramasse-miettes, et il cesse alors silencieusement de se déclencher. Cela arrive le plus souvent quand un timer est créé comme variable locale dans une méthode qui se termine.
Change(dueTime, period) replanifie le timer ; Timeout.Infinite pour les deux le met en pause, et une nouvelle paire le relance. Dispose() l'arrête définitivement.
System.Timers.Timer
System.Timers.Timer enveloppe le même mécanisme dans une API à base d'événements : définissez Interval, abonnez-vous à Elapsed, puis appelez Start() et Stop().
Exemple de sortie :
autosave #1
autosave #2
autosave #3
autosave stopped
AutoReset = false crée un timer à un seul coup : il se déclenche une fois puis s'arrête, et appeler à nouveau Start() planifie le tick unique suivant. Enabled = true et false équivalent à Start() et Stop(). Le nom complet System.Timers.Timer est écrit ici parce que System.Threading a aussi un Timer ; avec les deux namespaces importés, un Timer seul est ambigu et ne compile pas.
System.Timers.Timer a aussi une propriété SynchronizingObject qui transfère Elapsed sur un thread d'interface dans WinForms. En pratique, le timer propre au framework d'interface est plus simple pour cela.
Les callbacks se chevauchent et s'exécutent sur d'autres threads
Les deux classes ci-dessus se déclenchent sur le pool de threads, selon le planning, que le callback précédent soit terminé ou non. Si un callback prend 3 secondes et que la période est d'une seconde, trois callbacks s'exécutent en même temps. Cela crée deux obligations :
- Tout ce que touche le callback doit être thread-safe. Utilisez
Interlockedpour les compteurs (comme le font les exemples) et un lock pour tout ce qui est plus gros. - Protégez-vous contre le chevauchement quand il compte. Sautez un tick tant que le précédent tourne encore, ou utilisez un schéma qui ne peut pas se chevaucher.
Une garde qui saute le tick, avec 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);
}
}
Les exceptions sont l'autre piège. Une exception qui s'échappe d'un callback de System.Threading.Timer fait planter le processus, alors que System.Timers.Timer avale les exceptions des gestionnaires Elapsed, si bien que l'échec passe inaperçu. Enveloppez le corps d'un callback de timer dans un try/catch et journalisez.
PeriodicTimer et boucles async
Dans du code async, une boucle qui attend entre les itérations est le timer le plus clair : l'attente suivante ne commence qu'une fois le travail terminé, donc les ticks ne peuvent jamais se chevaucher, et les exceptions apparaissent là où vous pouvez les intercepter. .NET 6 a ajouté PeriodicTimer exactement pour cela :
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(30));
while (await timer.WaitForNextTickAsync(cancellationToken))
{
await SaveDraftAsync();
}
WaitForNextTickAsync renvoie false une fois le timer libéré, ce qui termine la boucle, et lève OperationCanceledException quand le jeton est annulé. Contrairement à une boucle autour de Task.Delay, un PeriodicTimer garde un rythme fixe : si le travail prend 2 secondes, le tick suivant arrive quand même 30 secondes après le précédent, pas 32.
Sur n'importe quelle version, la même forme avec Task.Delay fonctionne quand la dérive n'a pas d'importance :
Exemple de sortie :
poll 1
poll 2
poll 3
poll 4
polling cancelled after 4 rounds
L'annulation remplace Stop() : passez un jeton à la boucle et annulez-le depuis l'extérieur. Voir tâches pour en savoir plus sur les jetons d'annulation.
Mesurer un temps écoulé avec Stopwatch
Un timer planifie du code ; Stopwatch mesure combien de temps du code a pris. Il lit une horloge haute résolution qui ne fait qu'avancer, ce qui en fait le bon outil pour chronométrer :
Exemple de sortie :
string += 64 ms
StringBuilder 0 ms
Same length: True
High resolution clock: True
Start, Stop, Reset et Restart le contrôlent ; Elapsed est un TimeSpan, et ElapsedMilliseconds et ElapsedTicks donnent des nombres bruts. Évitez de chronométrer avec DateTime.Now : sa résolution peut être aussi grossière que 10 à 15 ms, et elle saute quand l'horloge système est réglée. Pour de vrais benchmarks, où le temps de chauffe du JIT et le ramasse-miettes faussent une exécution isolée, utilisez la bibliothèque BenchmarkDotNet.
Précision des timers
Un intervalle de timer est un minimum, pas une promesse. Sous Windows, la résolution par défaut du timer système est d'environ 15,6 ms, donc un intervalle de 10 ms tend à se déclencher toutes les 15 ou 16 ms, et sur une machine chargée un callback du pool de threads peut démarrer encore plus tard. Les timers conviennent pour « environ toutes les N secondes ». Pour une cadence d'images précise ou du multimédia, utilisez des API conçues pour cela.
Erreurs courantes
- Un
System.Threading.Timerstocké seulement dans une variable locale. Il peut être récupéré par le ramasse-miettes et cesser de se déclencher ; gardez-le dans un champ. - Laisser
Mainse terminer. Le processus s'arrête et emporte le timer avec lui. - Un
Timerambigu. AvecSystem.ThreadingetSystem.Timerstous deux importés, écrivez le nom complet. - Supposer que les ticks ne se chevauchent jamais. Les timers du pool de threads se chevauchent quand le travail est plus lent que l'intervalle.
- Toucher aux contrôles d'interface depuis un timer du pool de threads. Utilisez le timer du framework d'interface ou revenez sur le thread d'interface.
- Des exceptions non gérées dans les callbacks. Elles font planter le processus (
Threading.Timer) ou disparaissent (Timers.Timer) ; interceptez-les et journalisez. - Oublier
Dispose. Les timers détiennent des ressources système et continuent de se déclencher jusqu'à leur libération.
Questions fréquentes
Quel timer utiliser en C# ?
Dans du code async sur .NET 6 ou plus, PeriodicTimer avec await timer.WaitForNextTickAsync() dans une boucle. Pour un callback sur le pool de threads, System.Threading.Timer. Pour un timer à base d'événements avec Start, Stop et un événement Elapsed, System.Timers.Timer. Dans WinForms ou WPF, utilisez le timer du framework d'interface pour que le gestionnaire s'exécute sur le thread d'interface.
Comment exécuter du code toutes les quelques secondes en C# ?
Créez un timer avec l'intervalle, par exemple new System.Threading.Timer(_ => Refresh(), null, 0, 5000) pour appeler Refresh tout de suite puis toutes les 5 secondes, et gardez une référence vers lui. Dans du code async, bouclez avec PeriodicTimer (.NET 6+) ou await Task.Delay(5000) ; la version en boucle n'exécute jamais deux ticks à la fois.
Comment arrêter un timer en C# ?
Pour System.Timers.Timer, appelez Stop() (ou mettez Enabled = false), puis Dispose() quand vous n'en avez plus besoin. Pour System.Threading.Timer, appelez Change(Timeout.Infinite, Timeout.Infinite) pour le mettre en pause, ou Dispose() pour l'arrêter définitivement. Un callback déjà démarré peut quand même se terminer après l'arrêt du timer.
Pourquoi mon System.Threading.Timer cesse-t-il de se déclencher ?
Rien ne référence le timer, donc le ramasse-miettes l'a récupéré et ses callbacks se sont arrêtés. Cela arrive quand le timer est créé comme variable locale dans une méthode qui se termine. Stockez-le dans un champ aussi longtemps qu'il doit continuer à tourner.
Comment mesurer un temps écoulé en C# ?
Utilisez System.Diagnostics.Stopwatch : var sw = Stopwatch.StartNew(); ... sw.Stop(); puis lisez sw.ElapsedMilliseconds ou sw.Elapsed. Il utilise une horloge monotone haute résolution, contrairement à la soustraction de deux valeurs DateTime.Now, dont la résolution est plus grossière et qui saute quand l'horloge système est réglée.