Bien choisir entre les 3 minuteurs .NET - PeriodicTimer/Timer/DispatcherTimer
· Mis à jour le: · Go Komura · C#, .NET, WPF, Timer, Conception
Dans l’article précédent, Guide pratique pour réaliser autant que possible du soft real-time sur un Windows ordinaire - La checklist à consulter en premier, nous avons vu comment éviter les boucles périodiques reposant sur Sleep, au profit d’approches pilotées par événements et de waitable timers.
Alors, qu’en est-il du développement d’applications .NET plus ordinaire au quotidien ?
C’est là qu’on hésite facilement, entre PeriodicTimer, System.Threading.Timer et DispatcherTimer.
Ce sont tous des « minuteurs » par leur nom, mais :
- un minuteur dont on attend le tick avec
await - un minuteur dont le callback arrive via le ThreadPool
- un minuteur qui s’exécute sur le
Dispatcherdu thread UI
leurs caractères sont en réalité assez différents.
Voici, en gros, ce qui se mélange facilement dans la pratique.
- Passer un lambda
asyncàSystem.Threading.Timeralors que le traitement périodique est asynchrone - Toucher directement l’écran depuis un minuteur ThreadPool alors qu’il s’agit d’une mise à jour d’UI WPF
- Mettre un traitement lourd dans
DispatcherTimeret ralentir tout l’écran - Mélanger dans sa tête la discussion précédente sur le « soft real-time » et l’exécution périodique ordinaire d’une application
Cet article part principalement du principe d’applications C# / .NET classiques sous .NET 6 ou version ultérieure, et présente
PeriodicTimer / System.Threading.Timer / DispatcherTimer dans un ordre qui limite les hésitations au quotidien.
Les cibles envisagées sont, à peu près, les suivantes.
- Workers / services en arrière-plan
- Applications console
- Traitements en coulisses d’ASP.NET Core
- Applications de bureau WPF
Dans cet article, DispatcherTimer désigne principalement System.Windows.Threading.DispatcherTimer de WPF.
WinUI / UWP disposent aussi d’un DispatcherTimer reposant sur la même idée.
Pour WinForms, il est plus naturel de regarder du côté de System.Windows.Forms.Timer comme minuteur d’UI.
Notez que ce que nous traitons ici, c’est comment écrire l’exécution périodique côté application. Quand la précision de la période elle-même est le sujet, on revient à la discussion de l’article précédent sur le soft real-time.
Par ailleurs, le code présenté dans cet article est publié sur GitHub sous forme d’un ensemble d’exemples complets, compilables et exécutables (bibliothèques et démos console pour PeriodicTimer / System.Threading.Timer, ainsi que des tests unitaires vérifiant la fusion des ticks et le chevauchement des callbacks).
periodictimer-system-threading-timer-dispatchertimer-guide - komurasoft-blog-samples (GitHub)
Table des matières
- La conclusion d’abord (en une phrase)
- Tout tenir sur une seule page
- 2.1. Vue d’ensemble
- 2.2. Le tableau de décision de départ
- Ce qu’il faut distinguer en premier
- 3.1. Type callback ou type qui attend le tick ?
- 3.2. Exécution sur le ThreadPool ou sur le thread UI ?
- 3.3. Traitement périodique et garantie de précision sont deux sujets distincts
- Schémas typiques
- 4.1. Pour un traitement périodique async :
PeriodicTimer - 4.2. Pour un callback léger sur le ThreadPool :
System.Threading.Timer - 4.3. Pour une mise à jour d’UI WPF :
DispatcherTimer - 4.4. Pour un traitement périodique proche du soft real-time : voir d’autres outils
- 4.1. Pour un traitement périodique async :
- Anti-patterns courants
- Checklist de revue
- Aide-mémoire pour bien choisir
- Résumé
- Références
1. La conclusion d’abord (en une phrase)
- Si vous voulez écrire naturellement un traitement à intervalle régulier sur la base d’
await, commencez parPeriodicTimer - Si vous voulez déclencher périodiquement des callbacks légers sur le ThreadPool, utilisez
System.Threading.Timer - Si vous voulez mettre à jour l’écran sur le thread UI de WPF, utilisez
DispatcherTimer - Les callbacks de
System.Threading.Timerpeuvent se chevaucher. Y fourrer un traitement asynchrone sans précaution tourne vite au désordre DispatcherTimerpermet de toucher l’UI directement, mais en contrepartie, y mettre un traitement lourd bloque facilement toute l’UI- Dans le contexte du soft real-time évoqué précédemment, ces trois minuteurs ne sont pas les acteurs principaux de l’attente de haute précision
En résumé, voici les trois points à examiner en premier.
- Sur quel thread / contexte voulez-vous l’exécuter ?
- Voulez-vous écrire le corps du traitement de façon séquentielle avec
async/await? - Pouvez-vous tolérer le chevauchement des callbacks ?
Rien qu’en séparant ces trois points, on hésite déjà beaucoup moins.
2. Tout tenir sur une seule page
2.1. Vue d’ensemble
flowchart LR
A["Faire quelque chose à intervalle régulier"] --> B{"Voulez-vous l'exécuter sur le thread UI ?"}
B -- "Oui" --> C["DispatcherTimer"]
B -- "Non" --> D{"Voulez-vous écrire le corps<br/>simplement avec<br/>async / await ?"}
D -- "Oui" --> E["PeriodicTimer"]
D -- "Non" --> F{"Voulez-vous déclencher des<br/>callbacks légers sur le<br/>ThreadPool ?"}
F -- "Oui" --> G["System.Threading.Timer"]
F -- "Non" --> H["Envisager d'autres conceptions<br/>Channel / BackgroundService / event / waitable timer"]
Dans la pratique, cette ramification suffit la plupart du temps. En cas d’hésitation, le découpage le plus sûr à faire en premier est :
PeriodicTimer pour le traitement asynchrone, DispatcherTimer pour la mise à jour de l’UI.
System.Threading.Timer est pratique, mais avec ses particularités de chevauchement de callbacks et de gestion de durée de vie,
il est un peu délicat comme tout premier choix.
2.2. Le tableau de décision de départ
| Situation | Premier choix | Endroit d’exécution | Pourquoi ça convient | Première précaution |
|---|---|---|---|---|
| Exécuter à intervalle régulier des traitements async comme HTTP / BD / E/S fichier | PeriodicTimer |
Dans le flux de la méthode async en cours | S’écrit sur la base d’await ; l’arrêt et l’annulation sont naturels |
Suppose un minuteur pour un seul consommateur. Le retard n’est pas parallélisé automatiquement |
| Exécuter un léger heartbeat / envoi de métriques / vérification d’expiration de cache sur le ThreadPool | System.Threading.Timer |
ThreadPool | Léger et de type callback. Facile à intégrer dans une conception existante à base de callbacks | Le callback est supposé réentrant. Il peut se chevaucher. Conserver la référence |
| Exécuter à intervalle régulier l’affichage d’une horloge WPF ou une légère mise à jour d’UI | DispatcherTimer |
Le Dispatcher de WPF (thread UI) |
Peut toucher l’UI directement. Peut avoir une priorité | L’heure exacte de déclenchement n’est pas garantie. Un traitement lourd engorge l’UI |
La précision de la période est l’enjeu principal, et vous voulez éviter de vous reposer sur Sleep |
Ne pas faire de ces trois minuteurs les acteurs principaux | - | L’objectif n’est plus l’exécution périodique de l’application, mais la conception de la précision d’attente | Regarder du côté des events / waitable timers |
Ce qui compte dans ce tableau, c’est de regarder l’endroit d’exécution et la façon d’écrire, plus que le nom du minuteur. Quand le choix du minuteur tourne mal, c’est le plus souvent parce qu’on n’a pas regardé « où ça tourne », plutôt qu’à cause du nom de l’API.
3. Ce qu’il faut distinguer en premier
3.1. Type callback ou type qui attend le tick ?
Séparer ce point clarifie tout de suite les choses.
System.Threading.TimeretDispatcherTimersont de type callback / eventPeriodicTimerest du type où l’on attend le tick avecawait
Autrement dit :
- Le type callback, c’est « le minuteur qui vous appelle »
PeriodicTimer, c’est « vous qui attendez le tick suivant »
C’est là toute la différence.
Si le corps du traitement est async et que vous voulez lire « attendre → traiter → attendre à nouveau » comme un flux unique, PeriodicTimer est plus naturel.
À l’inverse, dans les cas où :
- vous voulez vous appuyer sur une conception existante à base de callbacks
- le corps du traitement est court et synchrone
- vous voulez simplement un déclenchement périodique
System.Threading.Timer convient.
PeriodicTimer est pratique, mais il n’est pas universel.
Il ne part pas du principe qu’on puisse lancer plusieurs WaitForNextTickAsync simultanément sur un même minuteur, et
si plusieurs ticks se produisent pendant qu’on n’attend pas, ils sont fusionnés en un seul.
Il est important de ne pas se méprendre en pensant qu’il « rattrape tout seul » le retard.
3.2. Exécution sur le ThreadPool ou sur le thread UI ?
Le point suivant à examiner, c’est où ça s’exécute.
Le callback de System.Threading.Timer s’exécute sur le ThreadPool, et non sur le thread qui l’a créé.
Il convient donc bien au traitement en arrière-plan, mais il n’est pas conçu pour toucher l’UI directement.
DispatcherTimer, à l’inverse, est un minuteur pour l’UI intégré à la file du Dispatcher.
Dans WPF, il s’exécute sur ce même Dispatcher, ce qui permet de mettre à jour l’UI directement à l’intérieur du gestionnaire Tick.
Cette différence est considérable.
- Pour toucher l’UI depuis un minuteur ThreadPool, il faut explicitement revenir sur l’UI
DispatcherTimerfacilite l’accès à l’UI, mais consomme en contrepartie du temps du thread UI
Autrement dit, le point fort de DispatcherTimer est de « pouvoir toucher l’UI en toute sécurité », mais
cela signifie aussi que « y mettre un traitement lourd entraîne avec lui les entrées et le nouveau rendu ».
3.3. Traitement périodique et garantie de précision sont deux sujets distincts
Ce point est important en tant que lien avec l’article précédent.
Même si l’expression « faire quelque chose à intervalle régulier » est la même, on a affaire à des problèmes différents selon qu’il s’agit :
- pour des raisons propres à l’application, de vouloir un traitement périodique toutes les quelques secondes
- ou de vouloir se rapprocher autant que possible d’un deadline à l’échelle de 1 ms à quelques ms
System.Threading.Timer est un minuteur léger et facile à manier, mais
ce n’est pas un outil dédié à la précision.
DispatcherTimer aussi subit l’influence de l’état de la file du Dispatcher et des priorités.
PeriodicTimer, lui aussi, peut sembler à son seul nom « garantir une période précise », mais
sa force en pratique tient moins à la precision qu’à la facilité d’écriture du flux async.
Il est donc plus sûr de séparer dès le départ si vous voulez :
- écrire l’exécution périodique de l’application, ou
- affiner la précision de l’attente
Quand ces deux sujets se mélangent, la discussion sur le choix du minuteur finit peu à peu par partir dans une direction étrange.
4. Schémas typiques
4.1. Pour un traitement périodique async : PeriodicTimer
Dans un worker, un BackgroundService, ou un traitement console qui reste résident,
si vous voulez exécuter un traitement async à intervalle régulier, PeriodicTimer est le plus facile à écrire en premier lieu.
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
public sealed class CacheRefreshWorker : BackgroundService
{
private readonly ILogger<CacheRefreshWorker> _logger;
public CacheRefreshWorker(ILogger<CacheRefreshWorker> logger)
{
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("CacheRefreshWorker started.");
await RefreshCacheAsync(stoppingToken);
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(5));
try
{
while (await timer.WaitForNextTickAsync(stoppingToken))
{
await RefreshCacheAsync(stoppingToken);
}
}
catch (OperationCanceledException)
{
_logger.LogInformation("CacheRefreshWorker stopping.");
}
}
private async Task RefreshCacheAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Refreshing cache...");
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
}
}
Ce qui est bien avec cette forme :
- Le flux du code est facile à suivre comme une seule méthode
async - Le
CancellationTokense transmet facilement tel quel en aval - On réduit la gestion de durée de vie et d’exceptions propre aux callbacks
En particulier, quand le corps du traitement consiste surtout en attente d’E/S, comme :
- appeler une API HTTP
- interroger une BD
- lire un fichier
- attendre (
await) d’autres API async
la compatibilité est vraiment bonne.
Il y a deux précautions à prendre.
- L’utiliser en supposant un minuteur pour un seul consommateur
- Décider soi-même de la politique à suivre quand le temps de traitement dépasse la période
Ce n’est pas parce que le traitement précédent a duré longtemps que PeriodicTimer va se paralléliser automatiquement pour rattraper le retard.
En ce sens, c’est un minuteur fait pour « écrire naturellement une boucle async à intervalle régulier ».
Si l’on va jusqu’à considérer la facilité de test, pouvoir utiliser le constructeur qui accepte un TimeProvider est aussi discrètement pratique.
4.2. Pour un callback léger sur le ThreadPool : System.Threading.Timer
Si tout ce que vous voulez, c’est appeler périodiquement un court callback, System.Threading.Timer est direct.
Par exemple, dans des cas comme :
- émettre un heartbeat
- relever des métriques légères
- insérer une courte vérification d’expiration
- s’accrocher à une conception existante à base de callbacks
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
public sealed class HeartbeatService : IHostedService, IDisposable
{
private readonly ILogger<HeartbeatService> _logger;
private Timer? _timer;
private int _running;
public HeartbeatService(ILogger<HeartbeatService> logger)
{
_logger = logger;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_timer = new Timer(OnTimer, null, TimeSpan.Zero, TimeSpan.FromSeconds(5));
return Task.CompletedTask;
}
private void OnTimer(object? state)
{
if (Interlocked.Exchange(ref _running, 1) != 0)
{
return;
}
try
{
_logger.LogInformation("Heartbeat: {Now}", DateTimeOffset.Now);
}
finally
{
Volatile.Write(ref _running, 0);
}
}
public Task StopAsync(CancellationToken cancellationToken)
{
_timer?.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
return Task.CompletedTask;
}
public void Dispose()
{
_timer?.Dispose();
}
}
Si cet exemple inclut Interlocked.Exchange, c’est parce que
System.Threading.Timer n’attend pas la fin du callback précédent.
Ce point est vraiment important.
- Le callback s’exécute sur le ThreadPool
- Le callback est supposé réentrant
- Si le traitement dure plus longtemps que l’intervalle, il peut se chevaucher
Si le traitement n’est pas léger, il est plus prudent de concevoir en :
- ignorant les déclenchements en double
- empilant le travail dans une file
- se rapprochant de
PeriodicTimer
Un autre point discrètement important : conserver la référence.
Même en cours de fonctionnement, un System.Threading.Timer devient éligible au GC dès qu’il n’a plus aucune référence.
De plus, même juste après avoir appelé Dispose(), un callback déjà mis en file peut encore s’exécuter par la suite.
Autrement dit, System.Threading.Timer est :
- léger
- rapide
- simple
mais en contrepartie, c’est un minuteur où vous devez proprement prendre en charge vous-même les contraintes du callback.
4.3. Pour une mise à jour d’UI WPF : DispatcherTimer
Si vous voulez mettre à jour périodiquement une horloge à l’écran ou un léger affichage d’état dans WPF, DispatcherTimer est naturel.
using System;
using System.Windows;
using System.Windows.Threading;
public partial class MainWindow : Window
{
private readonly DispatcherTimer _clockTimer;
public MainWindow()
{
InitializeComponent();
_clockTimer = new DispatcherTimer(DispatcherPriority.Background)
{
Interval = TimeSpan.FromSeconds(1)
};
_clockTimer.Tick += ClockTimer_Tick;
_clockTimer.Start();
}
private void ClockTimer_Tick(object? sender, EventArgs e)
{
ClockText.Text = DateTime.Now.ToString("HH:mm:ss");
}
protected override void OnClosed(EventArgs e)
{
_clockTimer.Stop();
_clockTimer.Tick -= ClockTimer_Tick;
base.OnClosed(e);
}
}
Ce qui est bien avec DispatcherTimer, c’est que Tick est traité sur le Dispatcher de WPF, ce qui permet de toucher l’UI directement.
Cela se marie bien, par exemple, avec des cas comme :
- l’affichage d’une horloge
- une légère mise à jour de l’affichage de l’état de connexion
- le déclencheur de réévaluation d’une Command
- une légère mise à jour d’une valeur affichée à l’écran
Il y a cependant, ici aussi, un point où l’ambiance change.
DispatcherTimer s’exécute sur le thread UI, donc
un traitement lourd dans le gestionnaire Tick entraîne directement avec lui les entrées, le rendu et la réorganisation, ce qui ralentit tout.
Par ailleurs, DispatcherTimer n’est pas un outil qui garantit un déclenchement « exactement à l’heure indiquée ».
Il subit l’influence des autres tâches présentes dans la file du Dispatcher et des priorités.
Aussi, en pratique, on gagne en stabilité en gardant à l’esprit au moins ceci :
- garder le contenu de Tick léger
- déporter ailleurs les E/S ou le CPU lourds
- à la fermeture, appeler
Stop()et se désabonner pour rendre la durée de vie explicite
4.4. Pour un traitement périodique proche du soft real-time : voir d’autres outils
C’est ici le point de jonction avec l’article précédent.
Ce que traitait l’article précédent sur le soft real-time, ce n’était pas « il suffit que ça tourne à peu près toutes les X secondes », mais comment réduire la gigue de la période et les deadline miss.
Dans ce contexte, les sujets principaux sont :
- ne pas se reposer sur une attente relative via
Sleep - utiliser une approche pilotée par événements ou des waitable timers
- séparer le fast path du slow path
- mesurer le retard
Il est donc plus propre de séparer le problème dès le départ, ainsi :
- traitement périodique async ordinaire d’une application
→
PeriodicTimer - callback ThreadPool
→
System.Threading.Timer - mise à jour d’UI
→
DispatcherTimer - la précision de la période elle-même est l’enjeu principal → l’univers de l’article précédent
La question « je veux que ça tourne aussi précisément que possible toutes les 1 ms, quel minuteur .NET choisir ? » n’est déjà plus, à moitié, une question de choix de minuteur, mais une question de méthode d’attente et de conception.
5. Anti-patterns courants
5.1. Passer directement un lambda async à System.Threading.Timer
C’est un piège dans lequel on tombe assez facilement.
_timer = new Timer(async _ => await RefreshAsync(), null,
TimeSpan.Zero, TimeSpan.FromSeconds(5));
Visuellement, c’est propre, mais TimerCallback est de type void.
Autrement dit, ce lambda async est en pratique traité comme un async void.
Il en résulte une situation plutôt boueuse :
- l’appelant ne peut pas faire await
- on ne peut pas attendre la fin
- la gestion des exceptions devient difficile
- il faut en plus gérer séparément le chevauchement des callbacks
Si le corps du traitement est async, mieux vaut d’abord envisager PeriodicTimer, qui se lit mieux.
5.2. Mettre un traitement lourd dans le Tick de DispatcherTimer
Comme DispatcherTimer permet de toucher l’UI directement, on est tenté d’y écrire n’importe quoi.
Mais c’est bien le thread UI.
- un long traitement synchrone
- un calcul CPU lourd
- des E/S bloquantes
- un traitement pouvant se déclencher deux fois et contenant un long
await
en y mettant l’un de ces éléments, on entre en collision frontale avec les entrées et le rendu de l’UI.
Il est plus stable de garder le contenu de Tick léger, de déporter le travail lourd en arrière-plan, et de ne renvoyer à l’UI que le résultat nécessaire.
5.3. Croire que PeriodicTimer rattrape automatiquement le retard
C’est également un malentendu facile.
PeriodicTimer est excellent comme outil pour écrire proprement une boucle async à intervalle régulier, mais
il n’exécute pas les itérations en parallèle de lui-même pour rattraper le retard quand le traitement précédent a duré longtemps.
Comme les ticks survenus pendant qu’on n’attendait pas peuvent être fusionnés en un seul, il faut décider par la conception :
- si l’on ignore le retard
- si seul l’état le plus récent compte
- ou si l’on veut absolument traiter chaque occurrence
5.4. Reporter à plus tard l’arrêt et la gestion de la durée de vie
Les minuteurs causent plus d’accidents à l’arrêt qu’au démarrage.
Voici ce qu’on a tendance à négliger.
- créer
System.Threading.Timercomme simple variable locale, sans conserver de référence - ne pas arrêter
System.Threading.Timeret laisser flou tout ce qui touche àDispose() - ne pas appeler
Stop()surDispatcherTimer, ni se désabonner de Tick - le minuteur qui prolonge la durée de vie de l’objet même après la fermeture de l’écran
DispatcherTimer en particulier peut maintenir en vie l’objet auquel la méthode est liée.
Si vous avez cette impression étrange de « tiens, cette Window aurait dû être fermée mais elle est toujours là », c’est ici qu’il faut chercher.
6. Checklist de revue
- Pouvez-vous expliquer si ce traitement périodique doit être écrit comme une mise à jour d’UI / un callback ThreadPool / une boucle async ?
- Le corps du traitement est-il async alors qu’on le force dans un minuteur de type callback ?
- Si vous utilisez
System.Threading.Timer, le code peut-il tolérer le chevauchement des callbacks, ou est-il protégé contre cela ? - Le Tick de
DispatcherTimercontient-il un traitement lourd, des E/S bloquantes, ou un long traitement synchrone ? - Si vous utilisez
PeriodicTimer, la politique à suivre en cas de retard est-elle décidée ? - La méthode d’arrêt (
Change/Dispose/Stop) et le déroulement à la fermeture de l’application sont-ils clairs ? - La référence à
System.Threading.Timerest-elle correctement conservée ? - Y a-t-il un désabonnement et un nettoyage pour
DispatcherTimerà la fermeture de l’écran ? - Le problème a-t-il été séparé dès le départ entre « exécution périodique de l’application » et « précision de l’attente » ?
7. Aide-mémoire pour bien choisir
Voici quelques repères pratiques.
-
Appeler une API toutes les 30 secondes pour mettre à jour la configuration →
PeriodicTimer -
Envoyer un heartbeat ou de légères métriques toutes les 5 secondes →
System.Threading.Timer -
Afficher une horloge ou effectuer de légères mises à jour de statut dans WPF →
DispatcherTimer -
Toucher directement l’UI à chaque Tick →
DispatcherTimer -
Le corps du traitement périodique est truffé d’
await, et vous voulez gérer naturellement l’arrêt et les exceptions →PeriodicTimer -
Ajouter à faible coût un petit déclenchement à base de callback →
System.Threading.Timer -
La précision de la période et la gestion de la gigue à l’échelle de 1 à 5 ms sont l’enjeu principal → avant ces trois minuteurs, regardez les méthodes d’attente de l’article précédent
Pour le dire de façon très abrupte en une ligne chacun :
PeriodicTimerest le minuteur pour l’asyncSystem.Threading.Timerest le minuteur pour les callbacks ThreadPoolDispatcherTimerest le minuteur pour l’UI
Avec ce moyen mnémotechnique, on se trompe rarement de beaucoup.
8. Résumé
Ce qui compte vraiment dans le choix d’un minuteur .NET, ce n’est pas la différence de nom, mais ces trois points.
- Où ça s’exécute
- Dans quel flux vous voulez l’écrire
- Comment gérer le chevauchement et les retards
Comme politique, cela suffit largement pour s’en sortir.
- Traitement périodique async :
PeriodicTimer - Callback léger sur le ThreadPool :
System.Threading.Timer - Mise à jour d’UI WPF :
DispatcherTimer - Si la précision est l’enjeu principal : regardez une autre méthode d’attente
Les minuteurs se mélangent parce que leurs noms se ressemblent. Mais leurs rôles ne se ressemblent pas tant que ça.
PeriodicTimerest un outil pour structurer le flux asyncSystem.Threading.Timerest un outil pour déclencher périodiquement des callbacksDispatcherTimerest un outil pour effectuer des mises à jour périodiques sur le thread UI
Rien qu’en pensant ces trois minuteurs séparément, le code devient beaucoup plus calme.
À l’inverse, quand ces points se mélangent, il arrive des choses assez classiquement pénibles :
- ce qui devrait être async devient une sorte d’
async void - ça plante à force de toucher l’UI directement
- les callbacks se chevauchent et l’état devient trouble
- même la discussion sur la précision de la période finit par se mêler au reste
Commencez par regarder « où vous voulez l’exécuter ». Cela suffit à rendre le choix du minuteur bien plus serein.
9. Références
- Ensemble complet du code d’exemple de cet article (bibliothèque, démo, tests unitaires) https://github.com/gomurin0428/komurasoft-blog-samples/tree/main/periodictimer-system-threading-timer-dispatchertimer-guide
- Article associé : Guide pratique pour réaliser autant que possible du soft real-time sur un Windows ordinaire - La checklist à consulter en premier
- Article associé : Bonnes pratiques async/await en C# - Le tableau de décision à consulter en premier
- Article associé : async/await et le thread UI pour WPF / WinForms en une page - où revient await, le Dispatcher, ConfigureAwait, et les blocages de .Result / .Wait()
- Timers - .NET
- PeriodicTimer Class
- PeriodicTimer.WaitForNextTickAsync(CancellationToken) Method
- PeriodicTimer.Dispose Method
- PeriodicTimer Constructor
- Timer Class (System.Threading)
- Timer Constructor (System.Threading)
- Background tasks with hosted services in ASP.NET Core
- DispatcherTimer Class (System.Windows.Threading)
- DispatcherTimer Class (Microsoft.UI.Xaml)
Articles associés
Articles récents partageant les mêmes étiquettes, pour approfondir des sujets proches.
Pourquoi utiliser le Generic Host .NET et BackgroundService dans une application de bureau
Comment utiliser le Generic Host et BackgroundService pour organiser le démarrage, le traitement périodique, l'arrêt, la journalisation, ...
CI/CD pratique pour les applications WinForms / WPF — Automatiser du build à la signature et à la distribution avec GitHub Actions
Guide pratique pour mettre en place le CI/CD des applications WinForms / WPF avec GitHub Actions. Couvre un YAML minimal de build+tests s...
Icônes de la zone de notification et notifications toast dans les applications Windows — les pièges de NotifyIcon et comment choisir la bonne AppNotification
Un guide pratique pour maintenir une application Windows métier résidente dans la zone de notification (system tray) et avertir l'utilisa...
Intégrer l'authentification Entra ID dans une application WinForms/WPF — Architecture pratique avec MSAL.NET et le broker WAM
Guide pratique pour intégrer l'authentification Entra ID (ex Azure AD) dans une application de bureau WinForms/WPF : la logique du client...
Tests UI automatisés pour applications de bureau Windows — le fonctionnement de UI Automation et la construction de tests robustes avec FlaUI
Un guide pratique des tests UI automatisés pour applications WinForms/WPF, en partant du fonctionnement de Windows UI Automation (l'arbor...
Sujets associés
Ces pages replacent le sujet dans un contexte plus large de services et de décisions.
Thèmes techniques Windows
Portail des sujets sur le développement Windows, l'analyse des incidents et la valorisation des actifs existants.
Thread UI et minuteries
Thread UI WPF / WinForms, flux asynchrones, Dispatcher et temporisation.
Services liés à ce sujet
Cet article est directement lié aux services suivants.
Développement d'applications Windows
Dans le développement d'applications Windows impliquant l'exécution périodique, les mises à jour d'UI et le traitement en arrière-plan, le choix du minuteur influe directement sur la qualité de l'implémentation.
Conseil technique et revue de conception
Si vous en êtes au stade où il faut clarifier la répartition des responsabilités entre PeriodicTimer et DispatcherTimer, cela peut faire l'objet d'un conseil technique et d'une revue de conception.
Questions fréquentes
Questions souvent posées lors d’une consultation sur le sujet de cet article.
- Quelle est la différence entre PeriodicTimer et System.Threading.Timer ?
- La plus grande différence est que PeriodicTimer est un type qui attend le tick via await, alors que System.Threading.Timer est un type à callback. Avec PeriodicTimer, on peut écrire « attendre → traiter → attendre à nouveau » comme le flux d'une seule méthode async, et il est facile de transmettre le CancellationToken en aval. À l'inverse, le callback de System.Threading.Timer s'exécute sur le ThreadPool et n'attend pas la fin du callback précédent, si bien qu'un traitement plus long que l'intervalle peut entraîner un chevauchement. PeriodicTimer convient pour un traitement périodique asynchrone, tandis que System.Threading.Timer convient pour un déclenchement périodique léger et synchrone via callback.
- PeriodicTimer rattrape-t-il automatiquement le retard lorsque le traitement prend du temps ?
- Non. Ce n'est pas parce que le traitement précédent a duré longtemps que PeriodicTimer va exécuter les itérations en parallèle pour rattraper le retard de lui-même. Si plusieurs ticks se produisent pendant que vous n'attendez pas, ils sont fusionnés en un seul. Il faut donc décider soi-même, au niveau de la conception, s'il faut ignorer le retard, ne considérer que l'état le plus récent, ou traiter absolument chaque occurrence. De plus, PeriodicTimer ne part pas du principe qu'on puisse lancer plusieurs WaitForNextTickAsync simultanément sur un même minuteur.
- Ne faut-il vraiment pas passer de lambda async à System.Threading.Timer ?
- Il vaut mieux l'éviter. Comme TimerCallback est de type void, le lambda async passé est en pratique traité comme un async void. L'appelant ne peut alors pas l'attendre avec await, ne peut pas attendre sa fin, la gestion des exceptions devient difficile, et il faut en plus gérer séparément le chevauchement des callbacks. Si le traitement principal est async, il est préférable d'envisager d'abord PeriodicTimer, qui est plus lisible et plus sûr.
- Dans quels cas faut-il utiliser DispatcherTimer ?
- On l'utilise lorsqu'on veut mettre à jour périodiquement l'UI dans WPF, par exemple pour l'affichage d'une horloge ou un léger indicateur de statut. Comme Tick est traité sur le Dispatcher de WPF (le thread UI), son point fort est de pouvoir toucher directement l'UI depuis le gestionnaire. Cependant, comme il s'exécute sur le thread UI, y placer un traitement lourd ou des E/S bloquantes entraîne aussi les entrées et le rendu, ce qui ralentit tout. De plus, le déclenchement exactement à l'heure indiquée n'est pas garanti. Il est plus stable de garder le contenu de Tick léger, de déporter le travail lourd en arrière-plan, et de rendre explicite la durée de vie en appelant Stop() et en se désabonnant à la fermeture de l'écran.
Profil de l’auteur
Page de présentation de l’auteur de l’article.
Go Komura
Représentant de KomuraSoft LLC
Spécialisé dans le développement de logiciels Windows, le conseil technique et l’analyse de pannes, notamment pour les systèmes existants et les incidents difficiles à reproduire.
Liens publics