Bien choisir entre les 3 minuteurs .NET - PeriodicTimer/Timer/DispatcherTimer

· Mis à jour le: · · 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 Dispatcher du 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.Timer alors 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 DispatcherTimer et 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

  1. La conclusion d’abord (en une phrase)
  2. Tout tenir sur une seule page
    • 2.1. Vue d’ensemble
    • 2.2. Le tableau de décision de départ
  3. 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
  4. 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
  5. Anti-patterns courants
  6. Checklist de revue
  7. Aide-mémoire pour bien choisir
  8. Résumé
  9. 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 par PeriodicTimer
  • 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.Timer peuvent se chevaucher. Y fourrer un traitement asynchrone sans précaution tourne vite au désordre
  • DispatcherTimer permet 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.

  1. Sur quel thread / contexte voulez-vous l’exécuter ?
  2. Voulez-vous écrire le corps du traitement de façon séquentielle avec async / await ?
  3. 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

OuiNonOuiNonOuiNonFaire quelque chose à intervalle régulierVoulez-vous l'exécuter sur le thread UI ?DispatcherTimerVoulez-vous écrire le corpssimplement avecasync / await ?PeriodicTimerVoulez-vous déclencher descallbacks légers sur leThreadPool ?System.Threading.TimerEnvisager d'autres conceptionsChannel / 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.Timer et DispatcherTimer sont de type callback / event
  • PeriodicTimer est du type où l’on attend le tick avec await

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
  • DispatcherTimer facilite 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 CancellationToken se 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.

  1. L’utiliser en supposant un minuteur pour un seul consommateur
  2. 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.Timer comme simple variable locale, sans conserver de référence
  • ne pas arrêter System.Threading.Timer et laisser flou tout ce qui touche à Dispose()
  • ne pas appeler Stop() sur DispatcherTimer, 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 DispatcherTimer contient-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.Timer est-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 :

  • PeriodicTimer est le minuteur pour l’async
  • System.Threading.Timer est le minuteur pour les callbacks ThreadPool
  • DispatcherTimer est 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.

  1. Où ça s’exécute
  2. Dans quel flux vous voulez l’écrire
  3. Comment gérer le chevauchement et les retards

Comme politique, cela suffit largement pour s’en sortir.

  1. Traitement périodique async : PeriodicTimer
  2. Callback léger sur le ThreadPool : System.Threading.Timer
  3. Mise à jour d’UI WPF : DispatcherTimer
  4. 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.

  • PeriodicTimer est un outil pour structurer le flux async
  • System.Threading.Timer est un outil pour déclencher périodiquement des callbacks
  • DispatcherTimer est 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

Articles récents partageant les mêmes étiquettes, pour approfondir des sujets proches.

Ces pages replacent le sujet dans un contexte plus large de services et de décisions.

Cet article est directement lié aux services suivants.

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.

Retour au blog