Самый точный таймер в .NET? - PullRequest
       7

Самый точный таймер в .NET?

29 голосов
/ 10 февраля 2012

Запуск следующего (слегка псевдо) кода приводит к следующим результатам.Я в шоке от того, насколько неточен таймер (прибавляет ~ 14 мс каждый Tick).

Есть ли что-нибудь более точное?

Ответы [ 9 ]

19 голосов
/ 10 февраля 2012

Для точного измерения времени необходимо использовать класс Секундомер MSDN

13 голосов
/ 24 июля 2015

Я также видел класс с точностью до 1 мс. Я взял код Ханса Пассанта с форума
https://social.msdn.microsoft.com/Forums/en-US/6cd5d9e3-e01a-49c4-9976-6c6a2f16ad57/1-millisecond-timer
и обернул его в класс для простоты использования в вашей форме. Вы можете легко настроить несколько таймеров, если хотите. В приведенном ниже примере кода я использовал 2 таймера. Я проверил это, и он работает нормально.

// 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!
        }
    }
}
10 голосов
/ 07 января 2016

Я думаю, что другие ответы не в состоянии адресовать , почему происходит 14мс в каждой итерации кода OP; это , а не из-за неточных системных часов (и DateTime.Now не является неточным, если вы не отключили службы NTP или не настроили неправильный часовой пояс или что-то глупое! Это всего лишь неточно ).

Точный таймер

Даже с неточными системными часами (использующими DateTime.Now, или с подключением солнечного элемента к АЦП, чтобы узнать, как высоко солнце в небе, или делением времени между пиковыми потоками, или ... ) код, следующий за этим шаблоном, будет иметь среднее отклонение по нулю (он будет совершенно точным с точностью в среднем ровно одну секунду между тиками):

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
}

(Если вы копируете и вставляете это, следите за случаями, когда ваш тиковый код выполняется дольше, чем interval. Я оставлю это в качестве упражнения для читателя, чтобы найти простые способы сделать это пропускает столько ударов, сколько нужно, чтобы nextTick приземлился в будущем)

Неточный таймер

Я предполагаю, что реализация Microsoft System.Threading.Timer следует за этим шаблоном. Этот шаблон всегда будет изменяться даже с совершенно точным и совершенно точным системным таймером (потому что требуется время, чтобы выполнить даже только операцию добавления):

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
}

Так что для людей, которые могут быть заинтересованы в использовании собственного таймера, не следуйте этой второй схеме.

Точное измерение времени

Как говорили другие авторы, класс Stopwatch дает большую точность для измерения времени, но совсем не помогает с точностью , если следовать неправильному шаблону. Но, , как сказал @Shahar , вы никогда не получите идеально точный таймер, поэтому вам нужно переосмыслить вещи, если совершенная точность - это то, что вы ' после.

Отказ от ответственности

Обратите внимание, что Microsoft мало говорит о внутренностях класса System.Threading.Timer , так что я очень умело размышляю об этом, но если это крякает как утка, то это, вероятно, утка. Кроме того, я понимаю, что этому уже несколько лет, но это все еще актуальный (и я думаю, без ответа) вопрос.

Редактировать: изменена ссылка на ответ @ Shahar

Редактировать: Microsoft имеет исходный код для многих онлайн-материалов, в том числе System.Threading.Timer , для всех, кому интересно посмотреть, как Microsoft реализовала этот таймер отключения

4 голосов
/ 10 февраля 2012

Настольные операционные системы (такие как windows) не операционные системы реального времени. Это означает, что вы не можете ожидать полной точности и не можете заставить планировщик запускать ваш код в точную миллисекунду, которую вы хотите. Особенно в приложении .NET, которое не является детерминированным ... например, в любое время, когда сборщик мусора может начать сбор, компиляция JIT может быть немного медленнее или немного быстрее ....

4 голосов
/ 10 февраля 2012

Таймер и DateTime не имеют достаточной точности для вашей цели.Попробуйте вместо этого Секундомер .Посмотрите следующую статью для более подробной информации:

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

3 голосов
/ 10 февраля 2012

Это не таймер, который является неточным, но DateTime.Now , который имеет заявленный допуск 16 мс.

Вместо этого я бы использовал свойство Environment.Ticks для измерения циклов ЦП во время этого теста.

Редактировать : Environment.Ticks также основан на системном таймере и может иметь те же проблемы с точностью, что и DateTime.Now.Я бы посоветовал выбрать StopWatch вместо этого, как упоминали многие другие ответчики.

0 голосов
/ 18 августа 2018

Это на самом деле не делает таймер более точным (как в , оно не гарантирует, что время между обратными вызовами составляет ровно 1 секунду ), но если все, что вам нужно, это таймер, который срабатывает один раз каждые секунды и не пропускают секунды из-за проблемы с дрейфом ~14ms (как показано в примере вывода OP между 17-й и 19-й секундами), вы можете просто изменить таймер на запуск в наступающей секунде, как только будет выполнен обратный вызов пожары (и, очевидно, вы могли бы сделать то же самое для наступающей минуты, наступающего часа и т. д., если все, что вам нужно, это убедиться, что интервал не смещается):

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 голосов
/ 30 января 2017

Вот другой подход.С точностью до 5-20 мс на моей машине.

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 голосов
/ 10 февраля 2012

Я сделал для этого урок, и, кажется, он работает просто отлично.Никаких неточностей:

class AccurateTimer
{

    public event EventHandler<EventArgs> Tick;

    public bool Running { get; private set; }
    public int Interval { get; private set; }

    public AccurateTimer(int interval_ = 1000)
    {
        Running = false;
        Interval = interval_;
    }

    public void Start()
    {
        Running = true;
        Thread thread = new Thread(Run);
        thread.Start();
    }

    public void Stop()
    {
        Running = false;
    }

    private void Run()
    {
        DateTime nextTick = DateTime.Now.AddMilliseconds(Interval);
        while (Running)
        {
            if (DateTime.Now > nextTick)
            {
                nextTick = nextTick.AddMilliseconds(Interval);
                OnTick(EventArgs.Empty);
            }
        }
    }

    protected void OnTick(EventArgs e)
    {
        EventHandler<EventArgs> copy = Tick;
        if (copy != null)
        {
            copy(this, e);
        }
    }

}

Возможно, это не лучшее решение.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...