web-dev-qa-db-fra.com

La minuterie la plus précise dans .NET?

L'exécution du code suivant (légèrement pseudo) produit les résultats suivants. Je suis choqué de voir à quel point le chronomètre est précis (gagne environ 14 ms chacun Tick).

Y at-il quelque chose de plus précis là-bas?

void Main()
{
   var timer = new System.Threading.Timer(TimerCallback, null, 0, 1000);
}

void TimerCallback(object state)
{
   Debug.WriteLine(DateTime.Now.ToString("ss.ffff"));
}

Sample Output:
...
11.9109
12.9190
13.9331
14.9491
15.9632
16.9752
17.9893
19.0043
20.0164
21.0305
22.0445
23.0586
24.0726
25.0867
26.1008
27.1148
28.1289
29.1429
30.1570
31.1710
32.1851
27
maxp

Pour mesurer le temps exact, vous devez utiliser la classe Chronomètre MSDN

18
RA.

J'ai aussi une classe qui est exacte à 1ms. J'ai pris le code de Hans Passant du forum
https://social.msdn.Microsoft.com/Forums/en-US/6cd5d9e3-e01a-49c4-9976-6c6a2f16ad57/1-millisecond-timer
et l’a enveloppé dans une classe pour faciliter son utilisation dans votre formulaire. Vous pouvez facilement configurer plusieurs minuteries si vous le souhaitez. Dans l'exemple de code ci-dessous, j'ai utilisé 2 minuteries. Je l'ai testé et ça marche bien. 

// AccurateTimer.cs
using System;
using System.Windows.Forms;
using System.Runtime.InteropServices;

namespace YourProjectsNamespace
{
    class AccurateTimer
    {
        private delegate void TimerEventDel(int id, int msg, IntPtr user, int dw1, int dw2);
        private const int TIME_PERIODIC = 1;
        private const int EVENT_TYPE = TIME_PERIODIC;// + 0x100;  // TIME_KILL_SYNCHRONOUS causes a hang ?!
        [DllImport("winmm.dll")]
        private static extern int timeBeginPeriod(int msec);
        [DllImport("winmm.dll")]
        private static extern int timeEndPeriod(int msec);
        [DllImport("winmm.dll")]
        private static extern int timeSetEvent(int delay, int resolution, TimerEventDel handler, IntPtr user, int eventType);
        [DllImport("winmm.dll")]
        private static extern int timeKillEvent(int id);

        Action mAction;
        Form mForm;
        private int mTimerId;
        private TimerEventDel mHandler;  // NOTE: declare at class scope so garbage collector doesn't release it!!!

        public AccurateTimer(Form form,Action action,int delay)
        {
            mAction = action;
            mForm = form;
            timeBeginPeriod(1);
            mHandler = new TimerEventDel(TimerCallback);
            mTimerId = timeSetEvent(delay, 0, mHandler, IntPtr.Zero, EVENT_TYPE);
        }

        public void Stop()
        {
            int err = timeKillEvent(mTimerId);
            timeEndPeriod(1);
            System.Threading.Thread.Sleep(100);// Ensure callbacks are drained
        }

        private void TimerCallback(int id, int msg, IntPtr user, int dw1, int dw2)
        {
            if (mTimerId != 0)
                mForm.BeginInvoke(mAction);
        }
    }
}

// FormMain.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace YourProjectsNamespace
{
    public partial class FormMain : Form
    {
        AccurateTimer mTimer1,mTimer2;

        public FormMain()
        {
            InitializeComponent();
        }

        private void FormMain_Load(object sender, EventArgs e)
        {
            int delay = 10;   // In milliseconds. 10 = 1/100th second.
            mTimer1 = new AccurateTimer(this, new Action(TimerTick1),delay);
            delay = 100;      // 100 = 1/10th second.
            mTimer2 = new AccurateTimer(this, new Action(TimerTick2), delay);
        }

        private void FormMain_FormClosing(object sender, FormClosingEventArgs e)
        {
            mTimer1.Stop();
            mTimer2.Stop();
        }

        private void TimerTick1()
        {
            // Put your first timer code here!
        }

        private void TimerTick2()
        {
            // Put your second timer code here!
        }
    }
}
11
John

Je pense que les autres réponses ne parviennent pas à répondre pourquoi il y a eu un décalage de 14 ms à chaque itération du code du PO; c'est pas à cause d'une horloge système imprécise (et DateTime.Now n'est pas inexact, sauf si vous avez désactivé les services NTP ou si le mauvais fuseau horaire est défini ou si vous avez un truc ridicule! Ce n'est que imprécis] .

Minuterie précise

Même avec une horloge système imprécise (en utilisant DateTime.Now, ou en connectant une cellule solaire à un ADC pour indiquer la hauteur du soleil dans le ciel, ou en divisant le temps entre les crêtes des marées, ou ...), code suivant motif aura une moyenne de zéro oscillation (il sera parfaitement précis avec exactement une seconde entre les ticks en moyenne):

var interval = new TimeSpan(0, 0, 1);
var nextTick = DateTime.Now + interval;
while (true)
{
    while ( DateTime.Now < nextTick )
    {
        Thread.Sleep( nextTick - DateTime.Now );
    }
    nextTick += interval; // Notice we're adding onto when the last tick was supposed to be, not when it is now
    // Insert tick() code here
}

(Si vous faites un copier-coller, veillez aux cas où l'exécution de votre code de tick est plus longue que interval. Je vais laisser cela au lecteur de trouver un moyen simple de le faire sauter autant de fois que nécessaire. comme il faut à nextTick pour atterrir à l’avenir)

Minuterie inexacte

J'imagine que l'implémentation de System.Threading.Timer par Microsoft suit ce type de modèle. Ce modèle aura toujours suivi même avec une minuterie système parfaitement précise et précise (car il faut du temps pour exécuter même l'opération d'ajout):

var interval = new TimeSpan(0, 0, 1);
var nextTick = DateTime.Now + interval;
while (true)
{
    while ( DateTime.Now < nextTick )
    {
        Thread.Sleep( nextTick - DateTime.Now );
    }
    nextTick = DateTime.Now + interval; // Notice we're adding onto .Now instead of when the last tick was supposed to be. This is where slew comes from
    // Insert tick() code here
}

Donc, pour les personnes qui pourraient être intéressées à lancer votre propre minuterie, ne suivez pas ce deuxième modèle.

Mesure précise du temps

Comme d'autres affiches l'ont dit, la classe Stopwatch donne une bonne précision précision pour la mesure du temps, mais n'aide pas du tout si précision est utilisée si le modèle est incorrect. Mais, comme @Shahar l'a dit ce n'est pas comme si vous alliez commencer par obtenir une minuterie parfaitement précise. Vous devez donc repenser les choses si vous êtes parfaits avec precision

Avertissements

Notez que Microsoft ne parle pas beaucoup des composants internes de la classe System.Threading.Timer . Je spécule donc de manière informée à ce sujet, mais s’il s’acharne comme un canard, c’est probablement un canard. En outre, je me rends compte que cela remonte à plusieurs années, mais la question reste pertinente (et je pense sans réponse).

Edit: lien modifié à la réponse de @ Shahar

Edit: Microsoft a du code source pour de nombreuses choses en ligne, y compris System.Threading.Timer , pour tous ceux qui sont intéressés à voir comment Microsoft a implémenté ce minuteur.

7
Matt Thomas

Minuterie et DateTime n'ont pas assez de précision pour votre objectif. Essayez le Chronomètre à la place. Consultez l'article suivant pour plus de détails: 

http://blogs.msdn.com/b/ericlippert/archive/2010/04/08/precision-and-accuracy-of-datetime.aspx

4
Botz3000

Les systèmes d'exploitation de bureau (tels que Windows) sont pas système d'exploitation en temps réel. ce qui signifie que vous ne pouvez pas vous attendre à une précision absolue ni forcer le planificateur à déclencher votre code exactement à la milliseconde souhaitée. Spécialement dans les applications .NET non déterministes ... Par exemple, chaque fois que le GC peut commencer à collecter, une compilation JIT peut être un peu plus lente ou un peu plus rapide ... 

4
Shahar Gvirtz

Ce n'est pas le minuteur qui est inexact, mais DateTime.Now , qui a une tolérance annoncée de 16 ms. 

Au lieu de cela, je voudrais utiliser la propriété Environment.Ticks pour mesurer les cycles du processeur lors de ce test. 

Edit: Environment.Ticks est également basé sur la minuterie du système et peut avoir les mêmes problèmes de précision que DateTime.Now. Je conseillerais de choisir StopWatch à la place de la plupart des autres répondants. 

3
Dr. ABT

Voici une autre approche. Précis à 5-20ms sur ma machine.

public class Run
{
    public Timer timer;

    public Run()
    {
        var nextSecond = MilliUntilNextSecond();

        var timerTracker = new TimerTracker()
        {
            StartDate = DateTime.Now.AddMilliseconds(nextSecond),
            Interval = 1000,
            Number = 0
        };

        timer = new Timer(TimerCallback, timerTracker, nextSecond, -1);
    }

    public class TimerTracker
    {
        public DateTime StartDate;
        public int Interval;
        public int Number;
    }

    void TimerCallback(object state)
    {
        var timeTracker = (TimerTracker)state;
        timeTracker.Number += 1;
        var targetDate = timeTracker.StartDate.AddMilliseconds(timeTracker.Number * timeTracker.Interval);
        var milliDouble = Math.Max((targetDate - DateTime.Now).TotalMilliseconds, 0);
        var milliInt = Convert.ToInt32(milliDouble);
        timer.Change(milliInt, -1);

        Console.WriteLine(DateTime.Now.ToString("ss.fff"));
    }

    public static int MilliUntilNextSecond()
    {
        var time = DateTime.Now.TimeOfDay;
        var shortTime = new TimeSpan(0, time.Hours, time.Minutes, time.Seconds, 0);
        var oneSec = new TimeSpan(0, 0, 1);
        var milliDouble = (shortTime.Add(oneSec) - time).TotalMilliseconds;
        var milliInt = Convert.ToInt32(milliDouble);
        return milliInt;
    }
}
0
BobsMyUncle

Cela ne rend pas le chronomètre plus précis (comme dans cela ne garantit pas que le temps entre les rappels est exactement de 1 seconde ), mais si vous avez simplement besoin d’un chronomètre qui se déclenche une fois par seconde et ne le fait pas. sautez des secondes à cause du problème de dérive ~14ms (comme illustré dans l'exemple de sortie d'OP entre la 17ème et 19ème seconde), vous pouvez simplement changer le minuteur pour déclencher au début de la seconde suivante dès que le rappel est déclenché (et vous pouvez évidemment le faire la même chose pour la minute, l’heure et ainsi de suite, si l’important est de s’assurer que l’intervalle ne dérive pas):

using System.Threading;

static Timer timer;

void Main()
{   
    // 1000 - DateTime.UtcNow.Millisecond = number of milliseconds until the next second
    timer = new Timer(TimerCallback, null, 1000 - DateTime.UtcNow.Millisecond, 0);
}

void TimerCallback(object state)
{   
    // Important to do this before you do anything else in the callback
    timer.Change(1000 - DateTime.UtcNow.Millisecond, 0);

    Debug.WriteLine(DateTime.UtcNow.ToString("ss.ffff"));
}

Sample Output:
...
25.0135
26.0111
27.0134
28.0131
29.0117
30.0135
31.0127
32.0104
33.0158
34.0113
35.0129
36.0117
37.0127
38.0101
39.0125
40.0108
41.0156
42.0110
43.0141
44.0100
45.0149
46.0110
47.0127
48.0109
49.0156
50.0096
51.0166
52.0009
53.0111
54.0126
55.0116
56.0128
57.0110
58.0129
59.0120
00.0106
01.0149
02.0107
03.0136
0
Mathijs Flietstra