微秒和毫秒 C# 计时器






4.95/5 (83投票s)
MicroTimer:一个 C# 中的微秒和毫秒计时器,其使用方式类似于 .NET 的 System.Timers.Timer。
- 下载控制台演示项目 (MicroTimerConsoleDemo.zip) - 8.18 KB
- 下载 WinForms 演示项目 (MicroTimerWinFormsDemo.zip) - 17.2 KB
- 下载源代码 (MicroLibrarySource.zip) - 1.54 KB
引言
任何使用 .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),其中包含三个类:MicroStopwatch
、MicroTimer
和 MicroTimerEventArgs
。请参见上面的“下载源代码”链接。
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 位)。在更快的机器上,性能有了显著提高。
UI 几乎不需要以毫秒级的间隔进行更新。纯粹为了演示的目的,“下载 WinForms 演示项目”链接提供了一个非常简单的 WinForms 应用程序,它使用 MicroTimer 更新 UI。下面的屏幕截图演示了该应用程序作为一个秒表(带微秒显示)运行,其中 UI 每 1111µs (1.111ms) 使用 ElapsedMicroseconds
进行更新。
摘要
MicroTimer 设计用于需要非常快速计时器(约 1ms 级别)的情况;然而,由于 Windows 操作系统的非实时性质,它永远无法做到完全准确。不过,由于没有其他微秒软件计时器可用,它为这项任务提供了一个合理的解决方案(尽管占用处理器资源,但在快速系统上相对准确)。
历史
- 2010 年 7 月 31 日 - 文章提交。