高精度定时器实现 z

1背景Permalink

.NET Framework 提供了四种定时器,然而其精度都不高(一般情况下 15ms 左右),难以满足一些场景下的需求。

在进行媒体播放、绘制动画、性能分析以及和硬件交互时,可能需要 10ms 以下精度的定时器。这里不讨论这种需求是否合理,它是确实存在的问题,也有相当多的地方在讨论,说明这是一个切实的需求。然而,实现它并不是一件轻松的事情。

这里并不涉及内核驱动层面的定时器,只分析在 .NET 托管环境下应用层面的高精度定时器实现。

Windows 不是实时操作系统,所以任何方案都无法绝对保证定时器的精度,只是能尽量减少误差。所以,系统的稳定性不能完全依赖于定时器,必须考虑失去同步时的处理。

2等待策略Permalink

想要实现高精度定时器,必然需要等待和计时两种基础功能。等待用来跳过一定时间间隔,计时可以进行时间检查,用以调整等待时间。

等待策略实际就是两种:

  • 自旋等待:让 CPU 空转消耗时间,占用大量 CPU 时间,但是时间高度可控。
  • 阻塞等待:线程进入阻塞状态,出让 CPU 时间片,在等待一定时间后再由操作系统调度回到运行状态。阻塞时不占用 CPU,然而需要操作系统调度,时间难以控制。

可以看到二者各有优劣,应该按照不同需求进行不同的实现。

而计时机制可以说能用的只有一种,就是Stopwatch类。它内部使用了系统 API QueryPerformanceCounter/ QueryPerformanceFrequency来进行高精度计时,依赖于硬件,它的精度可以高达几十纳秒,非常适合用来实现高精度定时器。

所以难点在于等待策略,下面先分析简单的自旋等待。

2.1自旋等待Permalink

可以使用Thread.SpinWait(int iteration)来进行自旋,也就是让 CPU 在一个循环里空转,iteration参数是迭代次数。.NET Framework 中不少同步构造都用到了它,用来等待一小段时间,减少上下文切换的开销。

这里很难根据iteration来计算消耗的时间,因为 CPU 速度可能是动态的。所以需要结合使用Stopwatch。伪代码如下:

var 等待开始时间 = 当前计时;
while ((当前计时 - 等待开始时间) < 需要等待的时间)
{
    自旋;
}

写成实际代码:

void Spin(Stopwatch w, int duration)
{
    var current = w.ElapsedMilliseconds;
    while ((w.ElapsedMilliseconds - current) < duration)
        Thread.SpinWait(10);
}

这里的w是一个已经启动的Stopwatch,为了演示简单使用了ElapsedMilliseconds属性,精度是毫秒级的,使用ElapsedTicks属性就可以获得更高的精度(微秒级)。

然而如前所述,这样精度高但是是以消耗 CPU 时间为代价的,这样实现定时器会让一个 CPU 核心满负荷工作(如果执行的任务也没有阻塞的话)。相当于浪费了一个核心,在有些时候不太现实(比如核心很少甚至是单核的虚拟机上),所以需要考虑阻塞等待。

2.2阻塞等待Permalink

阻塞等待会把控制权交给操作系统,这样就必须确保操作系统能够及时的将定时器线程调度回运行状态。默认情况下,Windows 的系统定时器精度是 15.625ms,也就是说时间切片是这个尺寸。如果线程阻塞,出让其时间片进行等待,再被调度运行的时间至少是一个切片 15.625ms。那么必须减少时间切片的长度,才有可能实现更高的精度。

可以通过系统 API timeBeginPeriod来修改系统定时器精度到 1ms(它内部使用了没有给出文档的NtSetTimerResolution,这个 API 可以修改到 0.5ms)。不需要的时候使用timeEndPeriod还原。

修改系统定时器精度有副作用。它会增加上下文切换的开销,增加耗电量,降低系统整体性能。然而,很多程序都不得不这么做,因为没有其它方式能获 得更高的定时器精度。比如基于 WPF 的程序(包括 Visual Studio)、使用 Chromium 内核的应用(Chrome、QQ)、多媒体播放器、游戏等等很多程序都会在一定时间内把系统定时器精度修改到 1ms。(查看方法见后面

所以实际上这个副作用在桌面环境已经成为常态。而且从 Windows 8 开始,这个副作用减弱了。

在 1ms 的系统定时器精度前提下,可以使用三种方式实现阻塞等待:

  • Thread.Sleep
  • WaitHandle.WaitOne
  • Socket.Poll

另外,多媒体定时器timeSetEvent也使用了阻塞的方式。

Thread.SleepPermalink

它的参数使用毫秒单位,所以最多只能精确到 1ms。不过事实上很不稳定,Thread.Sleep(1)会在 1ms 与 2ms 两种状态间跳动,也就是可能会产生 +1ms 多的误差。

实测发现,没有任务负载的情况下(纯粹循环调用Sleep(1)),阻塞时长稳定在 2ms;而有任务负载时,则至少会阻塞 1ms。这和其它两种阻塞方式不同,详见后文。

如果需要修正这个误差,可以在阻塞 n 毫秒时,使用Sleep(n-1),并通过Stopwatch计时,剩余等待时间用Sleep(0)Thread.Yield或自旋来补充。

Sleep(0)会出让剩余的 CPU 时间片给优先级相同的线程,而Thread.Yield是出让剩余的 CPU 时间片给运行在同一核心上的线程。在出让的时间片结束后,其会被重新调度。一般情况下,整个过程可以在 1ms 之内完成。

Thread.Sleep(0)Thread.Yield在 CPU 高负载情况下非常不稳定,实测可能会阻塞高达 6ms 时间,所以可能会产生更多的误差。因此误差修正最好通过自旋方式实现。

WaitHandle.WaitOnePermalink

WaitHandle.WaitOneThread.Sleep类似,参数也是毫秒单位。

不同之处是,没有任务负载的情况下(纯粹循环调用WaitOne(1)),阻塞时长稳定在 1.5ms;而有任务负载时,则可能仅阻塞近乎于 0 的时间(猜测是它仅阻塞到当前时间片结束,尚未找到具体的文档说明)。所以它阻塞的时长范围是 0 到 2ms 多。

WaitHandle.WaitOne(0)是用来测试等待句柄状态的,它并不阻塞,所以用它来进行误差修正类似于自旋,但不如直接使用自旋可靠。

Socket.PollPermalink

Socket.Poll方法的参数是以微秒为单位,理论上,它是使用了网卡的硬件来定时,精度很高。然而,由于阻塞的实现仍然要依赖线程,所以它也只能达到 1ms 的精度。

它的优势是比Thread.SleepWaitHandle.WaitOne要更稳定,误差也更小,可以不需要修正,但要占用一个 Socket 端口。

没有任务负载的情况下(纯粹循环调用Poll(1)),阻塞时长稳定在 1ms;而有任务负载时,则和WaitOne类似,可能仅阻塞近乎于 0 的时间。所以它阻塞的时长范围是 0 到 1ms 多。

Socket.Poll(0)是用来测试 Socket 状态的,但它会阻塞,而且可能阻塞高达 6ms,所以不能用它来进行误差修正。

timeSetEventPermalink

timeSetEvent和之前提到的timeBeginPeriod一样属于 winmm.dll 提供的多媒体定时器功能。它可以直接当作定时器使用,也是提供 1ms 的精度。在不需要的时候使用timeKillEvent来关闭。

它的稳定性和精度也很高,如果需要 1ms 的定时,而又不能使用自旋,那么这是最理想的方案。

虽然 MSDN 上说timeSetEvent是个过时的方法,应该用CreateTimerQueueTimer替换。但是CreateTimerQueueTimer精度和稳定性都不如多媒体定时器,所以在需要高精度的时候,只能使用timeSetEvent

3定时器实现Permalink

需要注意的是,无论自旋还是阻塞,显然定时器都应该运行在独立的线程,不能干扰使用方线程工作。而对于高精度定时器来说,触发事件以执行任务的线程一般都在定时器线程内,而不是再使用独立的任务线程。

这是因为高精度定时场景下,执行任务的时间开销很可能大于定时器的时间间隔,如果默认就在其它线程执行任务,可能导致占用大量线程。所以应该把控制权交给用户,让用户在需要的时候自行调度任务执行的线程。

3.1触发模式Permalink

由于在定时器线程执行任务,所以定时器的触发就产生了三种模式。以下是它们的说明和主循环伪代码:

固定时间框架
比如间隔 10ms,任务 7-12ms,则会按照等待 10ms 、任务 7ms、等待 3ms、任务 12ms(超时 2ms 失去同步)、任务 7ms、等待 1ms(回到同步)、任务 7ms、等待 3ms、… 进行。就是尽量按照设定好的时间框架来执行任务,只要任务不是始终超时,就可以回到原本的时间框架上。
var 下一帧时间 = 0;
while(定时器开启)
{
    下一帧时间 += 间隔时间;
    while (当前计时 < 下一帧时间)
    {
        等待;
    }
    触发任务;
}
可推迟时间框架
上面的例子会按照等待 10ms 、任务 7ms、等待 3ms、任务 12ms(超时,推迟时间框架 2ms)、任务 7ms、等待 3ms、… 进行。超时的任务会推迟时间框架。
var 下一帧时间 = 0;
while(定时器开启)
{
    下一帧时间 += 间隔时间;
    if (下一帧时间 < 当前计时)
        下一帧时间 = 当前计时
    while (当前计时 < 下一帧时间)
    {
        等待;
    }
    触发任务;
}
固定等待时间
上面的例子会按照等待 10ms、任务 7ms、等待 10ms、任务 12ms、等待 10ms、任务 7ms… 进行。等待时间始终不变。
while(定时器开启)
{
    var 等待开始时间 = 当前计时;
    while ((当前计时 - 等待开始时间) < 间隔时间)
    {
        等待;
    }
    触发任务;
}
// 或者:
var 下一帧时间 = 0;
while(定时器开启)
{
    下一帧时间 += 间隔时间;
    while (当前计时 < 下一帧时间)
    {
        等待;
    }
    触发任务;
    下一帧时间 = 当前计时;
}

如果使用多媒体定时器(timeSetEvent),它固定实现了第一种模式,而其它的等待策略能够实现全部三种模式,可以根据需求选择。

while循环中的等待可以使用自旋或阻塞,也可以结合它们来达到精度、稳定性和 CPU 开销的平衡。

另外,由上面的伪代码可以看出,这三种模式的实现可以统一,能够做到根据情况切换。

3.2线程优先级Permalink

最好把线程优先级调高,以保证定时器能够稳定工作,减少被抢占的机会。然而需要注意,这在 CPU 资源不足时可能导致低优先级线程的饥饿。也就是说不能让高优先级线程去等待低优先级线程改变状态,很有可能低优先级线程没有机会运行,导致死锁或类似死锁 的状态。(见一种类似的饥饿的例子

线程的最终优先级和进程的优先级有关,所以有时候也需要提高进程优先级(见 C# 中的多线程系列的线程优先级说明)。

4其它Permalink

还有两点需要注意:

  1. 线程安全:定时器在独立线程运行,其暴露的成员都应该实现线程安全,否则在定时器运行时调用可能会产生问题。
  2. 及时释放资源:多媒体定时器、等待句柄、线程等等这些都是系统资源,在不需要它们的时候应该及时释放/销毁。

如何查看系统定时器精度?Permalink

简单的查看可以使用Sysinternals工具包中的 ClockRes,它会显示如下信息:

Maximum timer interval: 15.625 ms
Minimum timer interval: 0.500 ms
Current timer interval: 15.625 ms

// 或

Maximum timer interval: 15.625 ms
Minimum timer interval: 0.500 ms
Current timer interval: 1.000 ms

如果是想查看哪些程序请求了更高的系统定时器精度,那么运行:

powercfg energy -duration 5

它会监视系统能耗 5s,然后在当前目录生成一个energy-report.html的分析报告,可以打开它查看。

找到里面的警告部分,会有平台计时器分辨率:未完成的计时器请求Platform Timer Resolution:Outstanding Timer Request)信息。



参考:

  1. http://www.codeproject.com/Articles/98346/Microsecond-and-Millisecond-NET-Timer
  2. http://www.codeproject.com/Articles/571289/Obtaining-Microsecond-Precision-in-NET
  3. http://www.pinvoke.net/default.aspx/winmm/timeSetEvent.html
  4. http://www.geisswerks.com/ryan/FAQS/timing.html
  5. http://omeg.pl/blog/2011/11/on-winapi-timers-and-their-resolution/
  6. https://randomascii.wordpress.com/2013/07/08/windows-timer-resolution-megawatts-wasted/
  7. http://www.windowstimestamp.com/description
时间: 2024-10-19 15:00:08

高精度定时器实现 z的相关文章

Linux时间子系统之六:高精度定时器(HRTIMER)的原理和实现

上一篇文章,我介绍了传统的低分辨率定时器的实现原理.而随着内核的不断演进,大牛们已经对这种低分辨率定时器的精度不再满足,而且,硬件也在不断地发展,系统中的定时器硬件的精度也越来越高,这也给高分辨率定时器的出现创造了条件.内核从2.6.16开始加入了高精度定时器架构.在实现方式上,内核的高分辨率定时器的实现代码几乎没有借用低分辨率定时器的数据结构和代码,内核文档给出的解释主要有以下几点: 低分辨率定时器的代码和jiffies的关系太过紧密,并且默认按32位进行设计,并且它的代码已经经过长时间的优化

使用linux内核hrtimer高精度定时器实现GPIO口模拟PWM,【原创】

关键词:Android  linux hrtimer 蜂鸣器  等待队列 信号量 字符设备 平台信息:内核:linux3.4.39 系统:android/android5.1平台:S5P4418  作者:庄泽彬(欢迎转载,请注明作者) 邮箱:[email protected] 程序描述:本文控制的设备是无源蜂鸣器,由于无源蜂鸣器是需要产生一定的频率的PWM才能够控制蜂鸣器,不像有源蜂鸣器,只需要提供高低电平就可以控制蜂鸣器.linux内核普通的定时器,由于具有一定的局限性,不能达到纳秒级别的定时

Linux下的hrtimer高精度定时器【转】

本文转载自:http://blog.csdn.net/waverider2012/article/details/38305785 hrtimer高精度定时器的interval由ktime_set(const long secs, const unsigned long nsecs)决定,可做到ns级.此处的例子为5ms interval: [cpp] view plain copy #include <linux/kernel.h> #include <linux/module.h&g

linux下jiffies定时器和hrtimer高精度定时器【转】

本文转载自:http://blog.csdn.net/dosculler/article/details/7932315 一.jiffies定时器,HZ=100,精度只能达到10ms. 注:采用jiffies+msecs_to_jiffies(xx ms);可做到ms级,不过精度不够 #include <Linux/jiffies.h>//DO-->jiffies调用头文件#include <linux/timer.h>  //DO-->timer_list结构体 st

关于linux hrtimer高精度定时器的使用注意事项

关于linux hrtimer高精度定时器的使用注意事项 需要注意:由于hrtimer本身没有interval周期的概念,如果要实现hrtimer的周期调用,方法1) 超时函数,调用hrtimer_start(, tim,HRTIMER_MODE_REL);即把timer根据此tim超时时间插入到timer_base的队列中, 并返回HRTIMER_NORESTART方法2) 超时函数,调用hrtimer_forward()或者hrtimer_forward_now(), 把hrtimer的_s

Linux 高精度定时器hrtimer 使用示例【转】

本文转载自:http://blog.csdn.net/dean_gdp/article/details/25481225 hrtimer的基本操作 Linux的传统定时器通过时间轮算法实现(timer.c),但hrtimer通过红黑树算法实现.在struct hrtimer里面有一个node域,类型为struct rb_node,这个域代表了hrtimer在红黑树中的位置. hrtimer_start hrtimer_start函数将一个hrtimer加入到一个按照到期时间排序的红黑树中,其主要

C#中自定义高精度Timer定时器的实例教程

Timer 用于以用户定义的事件间隔触发事件.Windows 计时器是为单线程环境设计的,其中,UI 线程用于执行处理.它要求用户代码有一个可用的 UI 消息泵,而且总是在同一个线程中操作,或者将调用封送到另一个线程. 使用此计时器时,请使用控件的Tick事件执行轮询操作,或在指定的时间内显示启动画面.每当 Enabled 属性设置为true且Interval属性大于0时,将引发Tick事件,引发的时间间隔基于Interval属性设置.System.Windows.Forms.Timer是应用于

Linux时间子系统之七:定时器的应用--msleep(),hrtimer_nanosleep()

我们已经在前面几章介绍了低分辨率定时器和高精度定时器的实现原理,内核为了方便其它子系统,在时间子系统中提供了一些用于延时或调度的API,例如msleep,hrtimer_nanosleep等等,这些API基于低分辨率定时器或高精度定时器来实现,本章的内容就是讨论这些方便.好用的API是如何利用定时器系统来完成所需的功能的. /**************************************************************************************

Linux时间子系统之四:定时器的引擎:clock_event_device

早期的内核版本中,进程的调度基于一个称之为tick的时钟滴答,通常使用时钟中断来定时地产生tick信号,每次tick定时中断都会进行进程的统计和调度,并对tick进行计数,记录在一个jiffies变量中,定时器的设计也是基于jiffies.这时候的内核代码中,几乎所有关于时钟的操作都是在machine级的代码中实现,很多公共的代码要在每个平台上重复实现.随后,随着通用时钟框架的引入,内核需要支持高精度的定时器,为此,通用时间框架为定时器硬件定义了一个标准的接口:clock_event_devic