.NET中有多少种定时器一文介绍过.NET中至少有6种定时器,但精度都不是特别高,一般在15ms~55ms之间。在一些特殊场景,可能需要高精度的定时器,这就需要我们自己实现了。本文将讨论高精度定时器实现的思路。

高精度定时器

一个定时器至少需要考虑三部分功能:计时、等待、触发模式。计时是进行时间检查,调整等待的时间;等待则是用来跳过指定的时间间隔。触发模式是指定时器每次Tick的时间固定还是每次定时任务时间间隔固定。比如定时器时间间隔10ms,定时任务耗时7ms,是每隔10ms触发一次定时任务,还是等定时任务执行完后等10ms再触发下一个定时任务。

计时

Windows提供了可用于获取高精度时间戳或者测量时间间隔的API。系统原生API是QueryPerformanceCounter (QPC)。在.NET种提供了System.Diagnostics.Stopwatch类获取高精度时间戳,它内部也是通过QueryPerformanceCounter (QPC)进行高精度计时。

QueryPerformanceCounter (QPC)使用硬件计数器作为其基础。硬件计时器由三个部分组成:时钟周期生成器、计数时钟周期的计数器和检索计数器值的方法。这三个分量的特征决定了QueryPerformanceCounter (QPC)的分辨率、精度、准确性和稳定性[1]。它的精度可以高达几十纳秒,用来实现高精度定时器基本没什么问题。

等待

等待策略通常有两种:

  • 自旋:让CPU空转等待,一直占用CPU时间。
  • 阻塞:让线程进入阻塞状态,出让CPU时间片,满足等待时间后切换回运行状态。

自旋等待

自旋等待可以使用Thread.SpinWait(int iteration)来实现,参数iteration是迭代次数。由于CPU速度可能是动态的,所以很难根据iteration计算消耗的时间,最好是结合Stopwatch使用:

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

由于自旋是以消耗CPU为代价的,上述代码运行时,CPU处于满负荷工作状态(使用率持续保持100%左右),因此短暂的等待可以考虑自旋,长时间运行的定时器不太建议使用该方法。

阻塞等待

阻塞等待需要操作系统能够及时把定时器线程调度回运行状态。默认情况下,Windows的系统的计时器精度为15ms左右。如果是线程阻塞,出让其时间片进行等待,然后再被调度运行的时间至少是一个时间切片15ms左右。要通过阻塞实现高精度计时,则需要减少时间切片的长度。Windows系统API提供了timeEndPeriod可以把计时器精度修改到1ms,在使用计时器服务之前立即调用timeEndPeriod,并在使用完计时器服务后立即调用timeEndPeriodtimeEndPeriodtimeEndPeriod必须成对出现。

在Windows 10, version 2004之前,timeEndPeriod会影响全局Windows设置,所有进程都会使用修改后的计时精度。从Windows 10, version 2004开始,只有调用timeEndPeriod的进程收到影响。

设置更高的精度可以提高等待函数中超时间隔的准确性。 但是,它也可能会降低整体系统性能,因为线程计划程序更频繁地切换任务。 高精度还可以阻止 CPU 电源管理系统进入节能模式。 设置更高的分辨率不会提高高分辨率性能计数器的准确性。[2]

通常我们使用Thread.Sleep来挂起线程等待,Sleep的参数最小为1ms,但实际上很不稳定,实测发现大部分时候稳定在阻塞2ms。我们可以采用Sleep(0)或者Thread.Yield结合Stopwatch计时的方式修正。

void wait(Stopwatch w, int duration)
{
var current = w.ElapsedMilliseconds;
while ((w.ElapsedMilliseconds - current) < duration)
Thread.Sleep(0);
}

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

还有一种阻塞的方式是多媒体定时器timeSetEvent,也是网上关于高精度定时器提得比较多的一种方式。它是winmm.dll中的函数,稳定性和精度都比较高,能提供1ms的精度。

官方文档中说timeSetEvent是一个过时的方法,建议使用CreateTimerQueueTimer替代[3]。但CreateTimerQueueTimer的精度和稳定性都不如多媒体定时器,所以在需要高精度定时器时,还是要用timeSetEvent。以下是封装多媒体定时器的例子

public enum TimerError
{
MMSYSERR_NOERROR = 0,
MMSYSERR_ERROR = 1,
MMSYSERR_INVALPARAM = 11,
MMSYSERR_NOCANDO = 97,
} public enum RepeateType
{
TIME_ONESHOT=0x0000,
TIME_PERIODIC = 0x0001
} public enum CallbackType
{
TIME_CALLBACK_FUNCTION = 0x0000,
TIME_CALLBACK_EVENT_SET = 0x0010,
TIME_CALLBACK_EVENT_PULSE = 0x0020,
TIME_KILL_SYNCHRONOUS = 0x0100
} public class HighPrecisionTimer
{
private delegate void TimerCallback(int id, int msg, int user, int param1, int param2); [DllImport("winmm.dll", EntryPoint = "timeGetDevCaps")]
private static extern TimerError TimeGetDevCaps(ref TimerCaps ptc, int cbtc); [DllImport("winmm.dll", EntryPoint = "timeSetEvent")]
private static extern int TimeSetEvent(int delay, int resolution, TimerCallback callback, int user, int eventType); [DllImport("winmm.dll", EntryPoint = "timeKillEvent")]
private static extern TimerError TimeKillEvent(int id); private static TimerCaps _caps;
private int _interval;
private int _resolution;
private TimerCallback _callback;
private int _id; static HighPrecisionTimer()
{
TimeGetDevCaps(ref _caps, Marshal.SizeOf(_caps));
} public HighPrecisionTimer()
{
Running = false;
_interval = _caps.periodMin;
_resolution = _caps.periodMin;
_callback = new TimerCallback(TimerEventCallback);
} ~HighPrecisionTimer()
{
TimeKillEvent(_id);
} public int Interval
{
get { return _interval; }
set
{
if (value < _caps.periodMin || value > _caps.periodMax)
throw new Exception("invalid Interval");
_interval = value;
}
} public bool Running { get; private set; } public event Action Ticked; public void Start()
{
if (!Running)
{
_id = TimeSetEvent(_interval, _resolution, _callback, 0,
(int)RepeateType.TIME_PERIODIC | (int)CallbackType.TIME_KILL_SYNCHRONOUS);
if (_id == 0) throw new Exception("failed to start Timer");
Running = true;
}
} public void Stop()
{
if (Running)
{
TimeKillEvent(_id);
Running = false;
}
} private void TimerEventCallback(int id, int msg, int user, int param1, int param2)
{
Ticked?.Invoke();
}
}

触发模式

由于定时任务执行时间不确定,并且可能耗时超过定时时间间隔,定时器的触发可能会有三种模式:固定时间框架,可推迟时间框架,固定等待时间。

  • 固定时间框架:尽量按照设定的时间来执行任务,只要任务不是始终超时,就可以回到原来的时间框架上
  • 可推迟时间框架:也是尽量按照设定的时间执行任务,但是超时的任务会推迟时间框架。
  • 固定等待时间:不管任务执行时长,每次任务执行结束到下一次任务开始执行间的等待时间固定。

假定时间间隔为10ms,任务执行的时间在7~11ms之间,下图中显示了三种触发模式的区别。

其实还有一种触发模式:任务执行时长大于时间间隔时,只要时间间隔一到,就执行定时任务,多个定时任务并发执行。之所以这里没有提及这种模式,是因为在高精度定时场景中,执行任务的时间开销很有可能大于定时器的时间间隔,如果开启新线程执行定时任务,可能会占用大量线程,这个需要结合实际情况考虑如何执行定时任务。这里讨论的是默认在定时器线程上执行定时任务。


  1. https://learn.microsoft.com/en-us/windows/win32/sysinfo/acquiring-high-resolution-time-stamps#low-level-hardware-clock-characteristics

  2. https://learn.microsoft.com/en-us/windows/win32/api/timeapi/nf-timeapi-timebeginperiod?redirectedfrom=MSDN

  3. https://learn.microsoft.com/en-us/previous-versions//dd757634(v=vs.85)?redirectedfrom=MSDN

.NET中如何实现高精度定时器的更多相关文章

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

    转自:http://blog.csdn.net/droidphone/article/details/8074892 上一篇文章,我介绍了传统的低分辨率定时器的实现原理.而随着内核的不断演进,大牛们已 ...

  2. 高精度定时器实现 z

    1背景Permalink .NET Framework 提供了四种定时器,然而其精度都不高(一般情况下 15ms 左右),难以满足一些场景下的需求. 在进行媒体播放.绘制动画.性能分析以及和硬件交互时 ...

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

    转自:http://blog.csdn.net/waverider2012/article/details/38305785 hrtimer高精度定时器的interval由ktime_set(cons ...

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

    关键词:Android  linux hrtimer 蜂鸣器  等待队列 信号量 字符设备 平台信息:内核:linux3.4.39 系统:android/android5.1平台:S5P4418  作 ...

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

    本文转载自:http://blog.csdn.net/dosculler/article/details/7932315 一.jiffies定时器,HZ=100,精度只能达到10ms. 注:采用jif ...

  6. iOS中的三种定时器

    iOS中的三种定时器 NSTimer 一.背景 定时器是iOS开发中经常使用的,但是使用不慎会造成内存泄露,因为NSTimer没有释放,控制器析构函数dealloc也没有调用,造成内存泄露. 二.使用 ...

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

    本文转载自:http://blog.csdn.net/dean_gdp/article/details/25481225 hrtimer的基本操作 Linux的传统定时器通过时间轮算法实现(timer ...

  8. Jmeter—7 测试中使用到的定时器和逻辑控制器

    1 测试中提交数据有延时1min,所以查询数据是否提交成功要设置定时器. 固定定时器页面:单位是毫秒 [dinghanhua] 2 集合点.Synchronizing Timer 集合点编辑:集合用户 ...

  9. C#中WebService 的 Timer定时器过段时间后自动停止运行

    我用.net做的一个Timer定时器,定时获取短信并给予回复,但大概过了十几个小时以后,Timer定时器会自动停止,再发送短信就不能收到回复,需要在服务器中重新运行定时器才可以,请教各位! 我是在.n ...

  10. Java 中Timer和TimerTask 定时器和定时任务使用的例子

    转自:http://blog.csdn.net/kalision/article/details/7692796 这两个类使用起来非常方便,可以完成我们对定时器的绝大多数需求 Timer类是用来执行任 ...

随机推荐

  1. MIT 6.828 Lab实验记录 —— lab1 Booting PC

    实验参考信息 MIT 6.828 lab1 讲义地址 MIT 6.828 课程 Schedule MIT 6.828 lab 环境搭建参考 MIT 6.828 lab 工具guide Brennan' ...

  2. 用一个示例来学习DockerFile

    在Docker的世界里,我们经常会听到Dockerfile这个词.那么,什么是Dockerfile?它如何工作?本文将简要介绍Dockerfile的基本概念,原理以及一些常用的Dockerfile命令 ...

  3. KRPANO PR10最新激活码(破解)分享

    KRPano pr10最新版本激活码下载地址: http://pan.baidu.com/s/1qYv2vO4 适用于最新pr10以及之前版本,解压密码为KRPano技术解密群群号:551278936 ...

  4. List取指定元素

    例如: List<string> list = new List<string>(); list.Take(50).ToList();//取前50条 list.Skip(10) ...

  5. Centos7使用ssh免密登陆同时禁用root密码登陆

    Centos7使用ssh免密登陆同时禁用root密码登陆 首先配置免密登陆,参考:ssh免密登陆 禁用root密码登陆 修改 /etc/ssh/sshd_config 文件 找到: RSAAuthen ...

  6. open3d -- voxel_down_sample

    官网文档 parameter: Input: open3d.geometry.Pointcloud点云类 voxel_size: 体素单位长度 Return: 处理后的点云类 Description: ...

  7. MySQL系列之——SQL介绍、常用SQL分类、数据类型、表属性、字符集、DDL应用、DCL应用、DML应用(增删改)、DQL应用(select )、元数据信息、show命令

    文章目录 一 SQL介绍 二 常用SQL分类 2.1 客户端命令 三 数据类型.表属性.字符集 3.1 数据类型 3.1.1 作用 3.1.2 种类 3.2 表属性 3.2.1 列属性 3.2.2 表 ...

  8. Python面向对象——property装饰器、继承(与python2不同点)、多继承(优缺点、Mixins)、属性查找、多继承带来的菱形问题

    文章目录 内容回顾 property装饰器 继承 与python2的差别 多继承 为何要用继承 如何实现继承 属性查找 多继承带来的菱形问题 总结: 作业 内容回顾 1.封装=>整合 人的对象. ...

  9. linux的进阶命令

    一. linux的基础命令 1.history 查看历史命令记录 2.ifconfig 查看所在的IP地址 3.tail -n 查看一个文件的后n行记录 4.head -n 查看一个文件的前n行记录5 ...

  10. Jmeter连接数据库sql语句操作,查询后取值做变量

    第一步 :导入jar包 第二步 :创建JDBC Reques 第三步 :创建JDBC Connection Configuration  第四步:在request中输入数据进行操作 Query Typ ...