65.9K
CodeProject 正在变化。 阅读更多。
Home

微秒和毫秒 C# 计时器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (83投票s)

2010年8月1日

CPOL

6分钟阅读

viewsIcon

487177

downloadIcon

21773

MicroTimer:一个 C# 中的微秒和毫秒计时器,其使用方式类似于 .NET 的 System.Timers.Timer。

引言

任何使用 .NET System.Timers.Timer 类进行低间隔时间设置的人都会发现,它的分辨率并不是很高。分辨率取决于系统,但通常的最大分辨率约为 15ms(System.Windows.Forms.Timer 的分辨率甚至更差,尽管 UI 不需要如此快速地更新)。使用 Win32 多媒体计时器(有各种 .NET 项目封装了这个计时器)可以实现显著更好的性能;然而,在微秒范围内没有可用的计时器。

我遇到的问题是,我需要每 800µs (0.8ms) 发送一个以太网 UDP 消息包;如果一个包稍有延迟或没有在距离上一个包正好 800µs 时发送,那也没关系。基本上,我需要的是一个大多数时候都准确的微秒计时器。

在 1ms 范围内进行软件计时器工作的根本问题在于,Windows 是一个非实时操作系统(RTOS),不适合在 1ms 标记附近生成规律且准确的事件。MicroTimer 不能也无法解决这个问题;然而,它确实提供了一个微秒计时器,该计时器在大多数时间(约 99.9%)提供了一定程度的准确性(约 1µs)。问题是,在 0.1% 的时间内,计时器可能会非常不准确(因为操作系统会将部分处理时间分配给其他线程和进程)。准确性高度依赖于系统/处理器;更快的系统会产生更准确的计时器。

MicroTimer 的优点在于,它的调用方式与现有的 System.Timers.Timer 类非常相似;然而,间隔是以微秒为单位设置的(而不是 System.Timers.Timer 中的毫秒)。在每次计时事件发生时,MicroTimer 会调用预定义的 (OnTimedEvent) 回调函数。MicroTimerEventArgs 属性提供信息(以微秒为单位),说明计时器确切的调用时间(以及延迟了多久)。

使用代码

'MicroLibrary.cs' 包含三个类(在 MicroLibrary 命名空间下)

  • MicroStopwatch - 它派生于并扩展了 System.Diagnostics.Stopwatch 类;重要的是,它提供了额外的 ElapsedMicroseconds 属性。这在作为独立类使用时非常有用,可以直接获取从秒表启动以来经过的微秒数。
  • MicroTimer - 设计成与 System.Timers.Timer 类非常相似,它有一个以微秒为单位的计时器间隔和 Start / Stop 方法(或 Enabled 属性)。该计时器实现了一个自定义事件处理程序 (MicroTimerElapsedEventHandler),该处理程序每间隔触发一次。NotificationTimer 函数是执行“工作”的地方,它在一个单独的高优先级线程中运行。需要注意的是,MicroTimer 效率低下且非常占用处理器资源,因为 NotificationTimer 函数运行一个紧密的 while 循环,直到经过的微秒数大于下一个间隔。while 循环使用 SpinWait,这并不是睡眠,而是运行几纳秒,有效地使线程休眠而不会放弃其 CPU 时间片的剩余部分。这不是理想的解决方案;然而,对于如此小的间隔,这可能是唯一实用的解决方案。
  • MicroTimerEventArgs - 派生于 System.EventArgs,此类提供一个用于保存事件信息的对象。具体来说,事件触发的次数、从计时器启动以来的绝对时间(以微秒为单位)、事件延迟了多久以及(上一个事件的)回调函数的执行时间。通过这些数据,可以推导出各种计时器信息。

根据设计,回调函数 (OnTimedEvent) 中执行的工作量必须很小(例如,更新一个变量或发送一个 UDP 包)。为此,回调函数中执行的工作量必须远远小于计时器间隔。可以为更长的任务生成单独的线程;然而,这超出了本文的范围。如前所述,由于 Windows 不是实时操作系统,回调函数 (OnTimedEvent) 可能会延迟;如果发生这种情况并且某个特定间隔被延迟,则有两种选择

  • 选项一:设置 IgnoreEventIfLateBy 属性,这样如果计时器延迟了指定的微秒数,回调函数 (OnTimedEvent) 将不会被调用。优点是计时器不会尝试“追赶”,即它不会快速连续地调用回调函数来试图追赶。缺点是会错过一些事件。
  • 选项二:默认情况下,MicroTimer 总是会尝试在下一个间隔进行追赶。优点是 OnTimeEvent 被调用的次数相对于总经过时间总是正确的(这就是为什么 OnTimedEvent 的执行时间必须远远小于间隔;如果执行时间相似或更长,MicroTimer 永远无法“追赶”,计时器事件将永远延迟)。缺点是当它试图“追赶”时,实际实现的间隔会远远小于所需的间隔,因为回调函数会快速连续地被调用以试图追赶。

计时器可以通过三种方式停止

  • Stop (或 Enabled = false) - 此方法通过设置一个标志来停止计时器,指示计时器停止,但是,此调用是异步执行的,即 Stop 调用会立即返回(但当前的计时器事件可能尚未完成)。
  • StopAndWait - 此方法同步停止计时器,它不会返回,直到当前计时器(回调)事件完成并且计时器线程终止。StopAndWait 还有一个重载方法,接受一个超时时间(以毫秒为单位),如果在超时时间内成功停止计时器,则返回 true,否则返回 false。
  • Abort - 此方法可能作为最后的手段来终止计时器线程,例如,如果计时器在等待 1 秒(1000 毫秒)后仍未停止,则可以使用
    if( !microTimer.StopAndWait(1000) ){ microTimer.Abort(); }

下面的代码显示了 MicroLibrary 命名空间 (MicroLibrary.cs),其中包含三个类:MicroStopwatchMicroTimerMicroTimerEventArgs。请参见上面的“下载源代码”链接。

using System;

namespace MicroLibrary
{
    /// <summary>
    /// MicroStopwatch class
    /// </summary>
    public class MicroStopwatch : System.Diagnostics.Stopwatch
    {
        readonly double _microSecPerTick =
            1000000D / System.Diagnostics.Stopwatch.Frequency;

        public MicroStopwatch()
        {
            if (!System.Diagnostics.Stopwatch.IsHighResolution)
            {
                throw new Exception("On this system the high-resolution " +
                                    "performance counter is not available");
            }
        }

        public long ElapsedMicroseconds
        {
            get
            {
                return (long)(ElapsedTicks * _microSecPerTick);
            }
        }
    }

    /// <summary>
    /// MicroTimer class
    /// </summary>
    public class MicroTimer
    {
        public delegate void MicroTimerElapsedEventHandler(
                             object sender,
                             MicroTimerEventArgs timerEventArgs);
        public event MicroTimerElapsedEventHandler MicroTimerElapsed;

        System.Threading.Thread _threadTimer = null;
        long _ignoreEventIfLateBy = long.MaxValue;
        long _timerIntervalInMicroSec = 0;
        bool _stopTimer = true;

        public MicroTimer()
        {
        }

        public MicroTimer(long timerIntervalInMicroseconds)
        {
            Interval = timerIntervalInMicroseconds;
        }

        public long Interval
        {
            get
            {
                return System.Threading.Interlocked.Read(
                    ref _timerIntervalInMicroSec);
            }
            set
            {
                System.Threading.Interlocked.Exchange(
                    ref _timerIntervalInMicroSec, value);
            }
        }

        public long IgnoreEventIfLateBy
        {
            get
            {
                return System.Threading.Interlocked.Read(
                    ref _ignoreEventIfLateBy);
            }
            set
            {
                System.Threading.Interlocked.Exchange(
                    ref _ignoreEventIfLateBy, value <= 0 ? long.MaxValue : value);
            }
        }

        public bool Enabled
        {
            set
            {
                if (value)
                {
                    Start();
                }
                else
                {
                    Stop();
                }
            }
            get
            {
                return (_threadTimer != null && _threadTimer.IsAlive);
            }
        }

        public void Start()
        {
            if (Enabled || Interval <= 0)
            {
                return;
            }

            _stopTimer = false;

            System.Threading.ThreadStart threadStart = delegate()
            {
                NotificationTimer(ref _timerIntervalInMicroSec,
                                  ref _ignoreEventIfLateBy,
                                  ref _stopTimer);
            };

            _threadTimer = new System.Threading.Thread(threadStart);
            _threadTimer.Priority = System.Threading.ThreadPriority.Highest;
            _threadTimer.Start();
        }

        public void Stop()
        {
            _stopTimer = true;
        }

        public void StopAndWait()
        {
            StopAndWait(System.Threading.Timeout.Infinite);
        }

        public bool StopAndWait(int timeoutInMilliSec)
        {
            _stopTimer = true;

            if (!Enabled || _threadTimer.ManagedThreadId ==
                System.Threading.Thread.CurrentThread.ManagedThreadId)
            {
                return true;
            }

            return _threadTimer.Join(timeoutInMilliSec);
        }

        public void Abort()
        {
            _stopTimer = true;

            if (Enabled)
            {
                _threadTimer.Abort();
            }
        }

        void NotificationTimer(ref long timerIntervalInMicroSec,
                               ref long ignoreEventIfLateBy,
                               ref bool stopTimer)
        {
            int  timerCount = 0;
            long nextNotification = 0;

            MicroStopwatch microStopwatch = new MicroStopwatch();
            microStopwatch.Start();

            while (!stopTimer)
            {
                long callbackFunctionExecutionTime =
                    microStopwatch.ElapsedMicroseconds - nextNotification;

                long timerIntervalInMicroSecCurrent =
                    System.Threading.Interlocked.Read(ref timerIntervalInMicroSec);
                long ignoreEventIfLateByCurrent =
                    System.Threading.Interlocked.Read(ref ignoreEventIfLateBy);

                nextNotification += timerIntervalInMicroSecCurrent;
                timerCount++;
                long elapsedMicroseconds = 0;

                while ( (elapsedMicroseconds = microStopwatch.ElapsedMicroseconds)
                        < nextNotification)
                {
                    System.Threading.Thread.SpinWait(10);
                }

                long timerLateBy = elapsedMicroseconds - nextNotification;

                if (timerLateBy >= ignoreEventIfLateByCurrent)
                {
                    continue;
                }

                MicroTimerEventArgs microTimerEventArgs =
                     new MicroTimerEventArgs(timerCount,
                                             elapsedMicroseconds,
                                             timerLateBy,
                                             callbackFunctionExecutionTime);
                MicroTimerElapsed(this, microTimerEventArgs);
            }

            microStopwatch.Stop();
        }
    }

    /// <summary>
    /// MicroTimer Event Argument class
    /// </summary>
    public class MicroTimerEventArgs : EventArgs
    {
        // Simple counter, number times timed event (callback function) executed
        public int  TimerCount { get; private set; }

        // Time when timed event was called since timer started
        public long ElapsedMicroseconds { get; private set; }

        // How late the timer was compared to when it should have been called
        public long TimerLateBy { get; private set; }

        // Time it took to execute previous call to callback function (OnTimedEvent)
        public long CallbackFunctionExecutionTime { get; private set; }

        public MicroTimerEventArgs(int  timerCount,
                                   long elapsedMicroseconds,
                                   long timerLateBy,
                                   long callbackFunctionExecutionTime)
        {
            TimerCount = timerCount;
            ElapsedMicroseconds = elapsedMicroseconds;
            TimerLateBy = timerLateBy;
            CallbackFunctionExecutionTime = callbackFunctionExecutionTime;
        }

    }
}

下面的代码显示了一个非常简单的(控制台应用程序)MicroTimer 类实现,将间隔设置为 1,000µs (1ms)。请参见上面的“下载控制台演示项目”链接。

using System;

namespace MicroTimerConsoleDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            Program program = new Program();
            program.MicroTimerTest();
        }

        private void MicroTimerTest()
        {
            // Instantiate new MicroTimer and add event handler
            MicroLibrary.MicroTimer microTimer = new MicroLibrary.MicroTimer();
            microTimer.MicroTimerElapsed +=
                new MicroLibrary.MicroTimer.MicroTimerElapsedEventHandler(OnTimedEvent);

            microTimer.Interval = 1000; // Call micro timer every 1000µs (1ms)

            // Can choose to ignore event if late by Xµs (by default will try to catch up)
            // microTimer.IgnoreEventIfLateBy = 500; // 500µs (0.5ms)

            microTimer.Enabled = true; // Start timer

            // Do something whilst events happening, for demo sleep 2000ms (2sec)
            System.Threading.Thread.Sleep(2000);

            microTimer.Enabled = false; // Stop timer (executes asynchronously)

            // Alternatively can choose stop here until current timer event has finished
            // microTimer.StopAndWait(); // Stop timer (waits for timer thread to terminate)

            // Wait for user input
            Console.ReadLine();
        }

        private void OnTimedEvent(object sender,
                                  MicroLibrary.MicroTimerEventArgs timerEventArgs)
        {
            // Do something small that takes significantly less time than Interval
            Console.WriteLine(string.Format(
                "Count = {0:#,0}  Timer = {1:#,0} µs, " + 
                "LateBy = {2:#,0} µs, ExecutionTime = {3:#,0} µs",
                timerEventArgs.TimerCount, timerEventArgs.ElapsedMicroseconds,
                timerEventArgs.TimerLateBy, timerEventArgs.CallbackFunctionExecutionTime));
        }
    }
}

下面的屏幕截图显示了控制台输出。性能在不同的运行中有所不同,但通常精确到 1µs。由于系统缓存,第一次运行时的准确性较差,在几次事件后有所改善。此测试在 2GHz 的 Dell Inspiron 1545 上运行,配备 Intel Core 2 Duo 处理器(运行 Windows 7 64 位)。在更快的机器上,性能有了显著提高。

MicroTimer/MicroTimerConsoleDemo.jpg

UI 几乎不需要以毫秒级的间隔进行更新。纯粹为了演示的目的,“下载 WinForms 演示项目”链接提供了一个非常简单的 WinForms 应用程序,它使用 MicroTimer 更新 UI。下面的屏幕截图演示了该应用程序作为一个秒表(带微秒显示)运行,其中 UI 每 1111µs (1.111ms) 使用 ElapsedMicroseconds 进行更新。

MicroTimer/MicroTimerWinFormsDemo.jpg

摘要

MicroTimer 设计用于需要非常快速计时器(约 1ms 级别)的情况;然而,由于 Windows 操作系统的非实时性质,它永远无法做到完全准确。不过,由于没有其他微秒软件计时器可用,它为这项任务提供了一个合理的解决方案(尽管占用处理器资源,但在快速系统上相对准确)。

历史

  • 2010 年 7 月 31 日 - 文章提交。
© . All rights reserved.