Dot.Blog

C#, XAML, WinUI, WPF, Android, MAUI, IoT, IA, ChatGPT, Prompt Engineering

.NET et ses timers. Prise de tête ou réelle utilité ?

Le Framework .NET est riche, trop parfois.
Il existe par exemple de nombreuses classes “Timer”. Laquelle utiliser ? Dans quel contexte ? Pour quels avantages ou inconvénients ?

 

 

Combien de classes ? Au moins 5 !

Lorsqu’on cherche toutes les classes Timer ou équivalente du Framework on en trouve ! Et pas qu’un peu puisqu’il y en a au moins 4 que j’ai recensées plus une cinquième dont je parlerai plus loin.

Quatre classes ? Oui !

  • System.Windows.Forms.Timer
  • System.Timers.Timer
  • System.Threading.Timer
  • System.Web.UI.Timer
  • System.Diagnostics.Stopwatch

Et même une 6ème ? Le multimédia Timer. Toutefois cette classe n’existe pas de base dans le Framework raison pour laquelle je ne la compte pas mais son importance est telle qu’il faut malgré tout l’évoquer.

Donc 5 classes plus une 6ème en kit on va dire…

N’est-ce pas un peu le souk ? Comment choisir ?

System.Windows.Forms.Timer

Soyons clairs dès le départ, ce Timer là n’est utilisable qu’en Windows Forms qui est un vieux Framework de présentation obsolète conçu pour faire des applications au look Windows 95/XP.

Très bien fait, assez agréable à utiliser il faut l’avouer, plus personne n’utilise Windows Forms sérieusement. Depuis Avalon, Vista, c’est WPF le minimum syndical de l’UI sous Windows, voire UWP/WinRT depuis Windows 8.0.

Mais bon, on sait ce que c’est, les habitudes, le rush du boulot, pas le temps de se former… alors je sais très bien que nombreux sont encore ceux qui font du Windows Forms. C’est mal, en général vous le savez, mais faut bien vivre ma pov’ dame…

Bon, là n’est pas la question puisque je parle TImer aujourd’hui. Le System.Windows.Forms.Timer est donc le Timer proposé pour les applications Windows Forms. En dehors de ce handicap assez lourd (puisqu’on ne peut pas l’utiliser ailleurs) ce Timer là n’est pas celui que vous utiliserez pour faire un Sequencer ou un métronome. Ou bien vous serez déçu (vos utilisateurs en tout cas).

Le code est déclenché non pas directement (donc pas de mode préemptif) car l’évènement fait l’objet d’un message, comme les autres messages Windows, traités dans une file d’attente par le thread de l’UI. Autant dire que la précision n’est pas au rendez-vous.

Laissons donc tomber cette vieillerie et passons à autre chose.

System.Timers.Timer

Déjà plus sérieux ce Timer là fait partie du Framework et non d’une librairie spécialisée comme le précédent. Il est donc utilisable dans tout code .NET.

Décrit par la documentation comme un Timer “server based” qui a été optimisé pour une utilisation en environnement multitâche, c’est le Timer le plus utilisé certainement sous .NET.

Par défaut le code de l’évènement sera invoqué sur un Worker Thread obtenu depuis le pool de .NET. Les instances mêmes du timer peuvent être accédées par différents threads de façon “safe”.

On trouve même dans ce timer un SynchronizingObject qui permet de définir un objet qui servira de marqueur de Thread à utiliser. L’évènement sera alors déclenché sur le Thread ayant servi à créer l’objet de synchronisation. Ce mode de fonctionnement est plus rarement utilisé et n’est qu’une option mais il peut s’avérer très utile notamment pour mettre à jour des objet d’UI bien entendu. Néanmoins point de magie, dans ce cas il faudra tout de même attendre que le Thread d’UI soit libre pour que l’évènement soit traité (une file d’attente est créée si besoin).

La seule garantie qu’on a est qu’il ne s’écoulera pas moins de temps que prévu entre deux Ticks. Mais plus c’est possible.

Encore une fois ce n’est pas avec ce Timer là qu’on fait un métronome mais c’est une classe sur laquelle on peut compter pour de nombreuses utilisations même en multithreading.

System.Threading.Timer

En voilà un bien curieux animal ! On s’attendrait à ce qu’il soit thread safe puisqu’apparaissant dans le namespace System.Threading, et bien non ! Alors que System.Timers.Timer lui l’est… Mystère du Framework !

Bien entendu on peut se débrouiller pour l’utiliser de façon thread safe mais ce n’est pas de base. De plus ce timer là n’a pas une API cohérente avec les autres. C’est lors de sa création qu’on passe directement un Callback au lieu d’enregistrer un Event.

L’un de ses avantages est de permettre l’utilisation d’un objet de Contexte qui sera ainsi accessible dans le callback.

L’exécution du callback se passe sur Worker Thread, si on utilise un objet de contexte il faudra s’assurer qu’il fonctionne bien dans un tel environnement et qu’il est donc thread-safe lui-même.

De son mode d’exécution le callback est assurément appelé à chaque Tick, aucun battement n’est sauté. S’il n’y a pas de Worker Thread disponibles dans le pool de .NET cela peut malgré tout induire des délais dans le traitement mais cette circonstance est plus théorique que pratique.

Ici pas d’objet de synchronisation… donc si vous devez mettre à jour un objet d’UI vous devrez vous assurer de le faire au travers un Invoke sur thread principal.

System.Web.UI.Timer

C’est un peu comme le timer de Windows Forms, celui-là est spécialisé aussi pour un environnement de développement particulier, ici ASP.NET.

La documentation MSDN le décrit comme un timer permettant d’exécuter des publications (postback) à intervalle régulier. On l’utilise aussi comme déclencheur pour un contrôle UpdatePanel qui sert à faire des mises à jour partielles asychrones de pages Web.

Ici peu importe la régularité métronomique, nous sommes dans un contexte très particulier et il n’y a guère le choix. Web.UI.Timer est le Timer qu’il faut utiliser. Mais on l’a compris son but est spécifique.

System.Diagnostics.Stopwatch

Le fait que ce timer apparaissent dans le namespace Diagnostics nous indique qu’il ne s’agit pas d’un timer banal utilisable n’importe où. C’est un outil de diagnostic.

C’est un timer mais sans évènement, il ne sert, comme son nom l’indique par ailleurs clairement, que de “stopwatch”, c’est à dire de chronomètre, c’est à dire d’outil pour mesurer un temps écoulé entre un top départ et un top d’arrivée.

On utilise ce timer pour mesure le temps d’exécution d’une portion de code le plus souvent.

La mauvaise méthode consiste à écrire quelque chose du genre :

//Mauvais ! ne pas utiliser ce genre de code ...
int start = System.DateTime.Now.Millisecond;
//le code à mesurer...
int stop = System.DateTime.Now.Millisecond;
int elapsedTime = stop - start;

 

.NET et son fonctionnement, l’optimisation du compilateur C#, la classe DateTime elle-même, tout cela comporte bien trop de spécificités floues pour que la mesure puisse avoir du sens, peut-être sauf à mesure des choses vraiment très longue de telle sorte que les imprécisions deviennent négligeables.

Mais puisqu’il ne faut pas le faire, ne le faisons pas tout simplement !

La bonne méthode consiste à utiliser la classe Stopwatch :

Stopwatch stopWatch = new Stopwatch();
stopWatch.Start();
//le code à mesurer...
stopWatch.Stop();
TimeSpan ts = stopWatch.Elapsed;

 

Ici nous obtiendrons une mesure très précise, non polluée par tous les aléas évoqués plus haut.

Et le métronome alors ?

Comme je l’ai indiqué, aucune de ces classes ne peut servir à créer un métronome. J’entends par là un timer dont les Ticks seraient fiables, constant, sans dérive. Avec Threading.Timer on a quelque chose d’assez fiable malgré tout. Mais si vous voulez écrire une application de musique, un sequenceur MIDI ou autre de ce genre, vous aurez besoin d’un timer ultra précis et régulier.

Les Timers .NET ne peuvent pas vraiment descendre en dessous de 15ms environ, cela étant du à des mécanismes internes de .NET.

Mais on peut avoir besoin d’une résolution plus grande, de l’ordre de la milliseconde par exemple. S’il s’agit d’échantillonner une source analogique (appareil de mesure souvent) on veut s’assurer de la précision de cette opération et les timers “normaux” n’offrent pas assez de résolution ni de précision.

un Timer multimédia permet à une application de déclencher un évènement avec la plus grande résolution possible sous Windows, résolution qui ne dépend que de la qualité du hardware (vitesse, stabilité…).

Hélas, aussi fourni soit-il et bien que les timers pleuvent comme un jour gris de novembre, il n’existe pas de classe toute faite pour utiliser les timers multimédia de Windows. C’est donc un timer en kit, tout est là mais il faut monter le meuble soi-même, une sorte de Windows-Ikea style.

Comment implémenter un Multimedia Timer ?

Voici un exemple de code créant une classe réutilisable autour du timer multimédia système de Windows :

public class MultimediaTimer : IDisposable
{
    private bool disposed = false;
    private int interval, resolution;
    private UInt32 timerId; 

    public MultimediaTimer()
    {
        Resolution = 5;
        Interval = 10;
    }

    ~MultimediaTimer()
    {
        Dispose(false);
    }

    public int Interval
    {
        get
        {
            return interval;
        }
        set
        {
            CheckDisposed();

            if (value < 0)
                throw new ArgumentOutOfRangeException("value");

            interval = value;
            if (Resolution > Interval)
                Resolution = value;
        }
    }

    // Note minimum resolution is 0, meaning highest possible resolution.
    public int Resolution
    {
        get
        {
            return resolution;
        }
        set
        {
            CheckDisposed();

            if (value < 0)
                throw new ArgumentOutOfRangeException("value");

            resolution = value;
        }
    }

    public bool IsRunning
    {
        get { return timerId != 0; }
    }

    public void Start()
    {
        CheckDisposed();

        if (IsRunning)
            throw new InvalidOperationException("Timer is already running");

        // Event type = 0, one off event
        // Event type = 1, periodic event
        UInt32 userCtx = 0;
        timerId = NativeMethods.TimeSetEvent((uint)Interval, (uint)Resolution, TimerCallback, ref userCtx, 1);
        if (timerId == 0)
        {
            int error = Marshal.GetLastWin32Error();
            throw new Win32Exception(error);
        }
    }

    public void Stop()
    {
        CheckDisposed();

        if (!IsRunning)
            throw new InvalidOperationException("Timer has not been started");

        StopInternal();
    }

    private void StopInternal()
    {
        NativeMethods.TimeKillEvent(timerId);
        timerId = 0;
    }

    public event EventHandler Elapsed;

    public void Dispose()
    {
        Dispose(true);
    }

    private void TimerCallback(uint id, uint msg, ref uint userCtx, uint rsv1, uint rsv2)
    {
        var handler = Elapsed;
        if (handler != null)
        {
            handler(this, EventArgs.Empty);
        }
    }

    private void CheckDisposed()
    {
        if (disposed)
            throw new ObjectDisposedException("MultimediaTimer");
    }

    private void Dispose(bool disposing)
    {
        if (disposed)
            return;

        disposed = true;
        if (IsRunning)
        {
            StopInternal();
        }

        if (disposing)
        {
            Elapsed = null;
            GC.SuppressFinalize(this);
        }
    }
}

internal delegate void MultimediaTimerCallback(UInt32 id, UInt32 msg, ref UInt32 userCtx, UInt32 rsv1, UInt32 rsv2);

internal static class NativeMethods
{
    [DllImport("winmm.dll", SetLastError = true, EntryPoint = "timeSetEvent")]
    internal static extern UInt32 TimeSetEvent(UInt32 msDelay, UInt32 msResolution, MultimediaTimerCallback callback, ref UInt32 userCtx, UInt32 eventType);

    [DllImport("winmm.dll", SetLastError = true, EntryPoint = "timeKillEvent")]
    internal static extern void TimeKillEvent(UInt32 uTimerId);

 

Ce code n’est qu’un exemple parmi d’autres qu’on peut trouver sur le Web en cherchant bien.

Celui-ci est bien conçu et montre comment faire, le propos de cet article étant bien de montrer la voie plus que de fournir une extension du Framework .NET.

La classe ci-dessus s’utilise de la façon suivante :

class Program
{
    static void Main(string[] args)
    {
        TestThreadingTimer();
        TestMultimediaTimer();
    }

    private static void TestMultimediaTimer()
    {
        Stopwatch s = new Stopwatch();
        using (var timer = new MultimediaTimer() { Interval = 1 })
        {
            timer.Elapsed += (o, e) => Console.WriteLine(s.ElapsedMilliseconds);
            s.Start();
            timer.Start();
            Console.ReadKey();
            timer.Stop();
        }
    }

    private static void TestThreadingTimer()
    {
        Stopwatch s = new Stopwatch();
        using (var timer = new Timer(o => Console.WriteLine(s.ElapsedMilliseconds), null, 0, 1))
        {
            s.Start();
            Console.ReadKey();
        }
    }

}

 

Comme on le remarque au passage le Timer est utilisé dans un using afin de s’assurer que les ressources seront bien relâchées. Mais tous les timers sont de cette nature, utilisant des ressources externes à .NET ils réclament d’être disposés lorsqu’on ne veut plus s’en servir.

Conclusion

Six timers : 2 spécialisés (Web ou Windows Forms), 2 tout à fait utilisables mais pas forcément très précis, un chronomètre et enfin un timer en kit pour obtenir un système précis…

.NET ne rend pas toujours les choix aisés. Heureusement la grande cohérence du Framework fait que de telles choses restent assez rares.

Mais choisir le “bon” timer pour le bon job, voilà un petit problème sur lequel je suis certain tout le monde ne s’était pas penché…

Maintenant vous en savez assez pour bien choisir !

J’oubliais… Il existe un autres timer, qui change de fonctionnement en cours de journée, mais il très facile à mettre en oeuvre, c’est le timer “montre du patron”. Le matin vous prenez DateTime et vous ajoutez systématiquement un gros quart d’heure parce que vous arrivez toujours trop tard, et le soir quand vous partez prenez un DateTime et cette fois-ci enlevez une bonne grosse heure, car vous partirez toujours trop tôt ! (Hélas il n’existe pas de version “salarié” qui soit homologuée par le Medef…). je ne donne pas le code C# car il est assez basique au point qu’il pourrait même être écrit par votre chef, c’est dire.

Allez filez, vous êtes à la bourre ! Sourire

Stay Tuned !

Faites des heureux, partagez l'article !
blog comments powered by Disqus