Imprécision DispatcherTimer

cs_boumboum Messages postés 34 Date d'inscription samedi 8 mars 2003 Statut Membre Dernière intervention 4 décembre 2023 - 11 nov. 2022 à 15:24
Whismeril Messages postés 18939 Date d'inscription mardi 11 mars 2003 Statut Contributeur Dernière intervention 22 février 2024 - 12 nov. 2022 à 10:12

Bonjour,

Je suis sous VB.NET WPF. Je cherche à créer un bout de code qui s'execute périodiquement. Pour cela j'essaie d'utiliser la classe DispatcherTimer.

Pour l'instant ce n'est qu'un essai, mais je me rends compte que mon comptage jusque 200 soit 60 secondes car intervalle du timer à 300ms prend en réalité 82s aprés chronométrage. ce qui fait un sacrée imprécision quand même.

Imports System.Windows.Threading

Class MainWindow
    Public WithEvents Timer1 As New DispatcherTimer With {.Interval = New TimeSpan(0, 0, 0, 0, 300)}
    Public x As Double

    Private Sub Timer_Tick(sender As Object, e As EventArgs) Handles Timer1.Tick
        x += 1
        lbl1.Content = x
        If x = 200 Then Timer1.Stop()
    End Sub

    Private Sub bp_Click(sender As Object, e As RoutedEventArgs) Handles bp.Click
        Timer1.Start()
    End Sub
End Class

Je me demande si je fais bien. Sinon quelle pourrait être la solution car en fait j'aimerai avoir un intervalle de l'ordre de 20ms. J'ai cherché un peu mais pour l'instant je n'ai pas la solution. Même en mettant un interval à 1 seconde j'ai un écart non négligeable. Ce qui est quand même fort de café pour une tâche censée s'executer a peu prés précisément.

Quand je fais la même chose chose en Winforms en utilisant le contrôle Timer c'est déjà beaucoup plus précis. Mais j'aimerai le réaliser en WPF car cela va s'intégrer dans une appli déjà construite.

Merci pour votre coup de main.


4 réponses

Whismeril Messages postés 18939 Date d'inscription mardi 11 mars 2003 Statut Contributeur Dernière intervention 22 février 2024 649
11 nov. 2022 à 16:50

Bonjour

Il faut que tu prennes en comptes un certain nombre de paramètres

  1. Windows n'est pas particulièrement collaboratif pour faire des actions précises dans le temps
  2. Un timer quel qu'il soit n'est pas fait pour être précis, une fois son code effectué, il attend la durée de son intervalle, puis demande à Windows le droit d'exécuter à nouveau son code. Dans le cas d'un, c'est le dispacther qui s'en occupe avec, par défaut, une priorité "par défaut".
  3. Le code qu'exécute le timer prend un certain temps, qui peut être relativement stable s'il est "simple" et ne dépend pas trop de grand-chose, mais il peut aussi être très variable si tu appelles des méthodes qui dépendent d'accès disques, d'autres threads ou que sais-je encore.

Donc supposons un intervalle de 20 ms et un code qui a une durée stable d'environ 5 ms. Ça nous fait déjà une période de 25 ms.

Si à 24 ms le dispatcher est autorisé par Windows à lancer un thread, il ne lancera pas le code du Timer, mais un autre thread. Il attendra que Windows lui donne un nouveau fil d'exécution pour se demander s'il exécute le code du Timer et là ça va se jouer à la priorité.

La solution qui se rapprochera le plus d'une périodicité stable est

  • Utiliser un "vrai" thread, soit celui de system.Threading ou un du pool de thread.
  • Lui donner une priorité la plus haute, raisonnablement possible (Highest c'est vraiment super important, AboveNormal, est un compromis entre Highest et Normal).
  • Déclencher un stopwacth avant d'exécuter le code.
  • Après l'exécution du code selon la durée mesurée par le stopwatch
    • Si inférieure à 20 ms, boucler avec un while qui incrémente une valeur (attention à bien la remettre à 0 au début pour ne pas avoir un dépassement de capacité), ne pas mettre de thread.sleep car celui-ci est soumis à la même règle que le timer, il attend et demande à Windows le droit de redémarrer le thread
    • Si égal à 20 ms, relancer le code immédiatement
    • Si supérieure à 20 ms, à toi de voir si on attend 40 ms....

Mais malgré ça, tu resteras à la merci de Windows qui pourra choisir de mettre ton fil d'exécution en pause s'il veut et quand il veut.


Quand j'étais petit, la mer Morte n'était que malade.
George Burns

0
Whismeril Messages postés 18939 Date d'inscription mardi 11 mars 2003 Statut Contributeur Dernière intervention 22 février 2024 649
11 nov. 2022 à 17:58

Un truc dans ce genre

le xaml

<Window x:Class="WpfApp1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp1"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800" DataContext="{Binding}">
    <Grid DataContext="{Binding .}">
        <CheckBox IsChecked="{Binding IsRunning}" VerticalAlignment="top" Margin="5" Height="20" Width="150">
            <CheckBox.Style>
                <Style TargetType="CheckBox">
                    <Setter Property="Content" Value="Démarrer"/>
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding IsRunning}" Value="True">
                            <Setter Property="Content" Value="Arrêter"/>
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </CheckBox.Style>
        </CheckBox>

        <TextBlock Text="{Binding ValeurTest, StringFormat='Nombre de passages dans la boucle {0}'}" Margin="20" HorizontalAlignment="Center"/>

        <TextBlock Text="{Binding Duree, StringFormat='Durée d exécution de la boucle {0} ms'}" Margin="40" HorizontalAlignment="Center"/>

    </Grid>
</Window>

le code behind de la fenêtre

using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace WpfApp1
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            TestBoumBoum test = new TestBoumBoum();

            this.DataContext = test;
        }
    }
}

La classe

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
using System.Diagnostics;
using System.ComponentModel;

namespace WpfApp1
{
    internal class TestBoumBoum:INotifyPropertyChanged
    {

        public event PropertyChangedEventHandler? PropertyChanged;

        Thread t;
		Stopwatch chronoInterne = new Stopwatch();
        Stopwatch chronoExterne = new Stopwatch();

        private bool isRunning = false;
		/// <summary>
		/// Etat bindé sur un checkbox pour lancer le "timer"
		/// </summary>
		public bool IsRunning
		{
			get { return isRunning; }
			set
			{
				if (isRunning == value)
					return;

				isRunning = value;
				if (isRunning)//on lance le timer
				{
					ValeurTest = 0;
					t = new Thread(new ThreadStart(Boucle));
					t.Priority = ThreadPriority.BelowNormal;//à toi de voir si c'est la bonne priority
					t.Start();
				}
				else
                    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Duree"));

                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("IsRunning"));
            }

        }

		private TimeSpan intervalle = TimeSpan.FromMilliseconds(100);
		/// <summary>
		/// Intervalle entre 2 exécutions de code
		/// </summary>
		public TimeSpan Intervalle
		{
			get { return intervalle; }
			set { intervalle = value; }
		}

		private int valeur = 0;

		/// <summary>
		/// Valeur qui sera modifiée par le code du "timer" pour compter le mobre d'exécution lors de ton propre chronométrage
		/// </summary>
		public int ValeurTest
		{
			get { return valeur; }
			set
			{
				if (value == valeur)
					return;

				valeur = value;

				PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("ValeurTest"));
			}
		}

		/// <summary>
		/// Durée entre le start et le stop, pour calculer la stabilité
		/// </summary>
		public double Duree { get { return chronoExterne.Elapsed.TotalMilliseconds; } }

		private void Boucle()
		{
			chronoExterne.Restart();
			while(isRunning)//boucle générale, ton timer en somme
			{
                chronoInterne.Restart();

				//ton code à exécuter
				ValeurTest++;

				int attente = 0;
				//Console.WriteLine(chronoInterne.Elapsed.TotalMilliseconds);
				while(chronoInterne.Elapsed <= intervalle)//ça c'est pour passer le temps, attention si la durée de ton code est plus longue que 20 ms, dans ce cas on relance de suite.
					attente++;
			}
		}


	}
}

Quand j'étais petit, la mer Morte n'était que malade.
George Burns

0
cs_boumboum Messages postés 34 Date d'inscription samedi 8 mars 2003 Statut Membre Dernière intervention 4 décembre 2023
12 nov. 2022 à 08:39

Merci beaucoup. Je vais déjà essayer de comprendre tout cà.


0
Whismeril Messages postés 18939 Date d'inscription mardi 11 mars 2003 Statut Contributeur Dernière intervention 22 février 2024 649
12 nov. 2022 à 10:12

Ha zut, un peu pressé hier, j'ai posté le code en C# et pas en VB.Net.

Tu peux le convertir là

https://lite.qwant.com/?q=C%23+to+vb&client=opensearch


Quand j'étais petit, la mer Morte n'était que malade.
George Burns

0
Rejoignez-nous