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

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

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

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

Ответы

Ответ 1

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

Ответ 2

Я также пропустил класс с точностью до 1 мс. Я взял код Hans Passant с форума
 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!
        }
    }
}

Ответ 3

Я думаю, что другие ответы не позволяют решить, почему 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 реализовала этот таймер с таймером

Ответ 5

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

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

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

Ответ 6

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

Ответ 7

Вот еще один подход. Точно в течение 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;
    }
}

Ответ 8

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

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);
        }
    }

}

Это может быть не лучшее решение.