.NET中如何实现高精度定时器
.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,并在使用完计时器服务后立即调用timeEndPeriod。timeEndPeriod和timeEndPeriod必须成对出现。
在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之间,下图中显示了三种触发模式的区别。

其实还有一种触发模式:任务执行时长大于时间间隔时,只要时间间隔一到,就执行定时任务,多个定时任务并发执行。之所以这里没有提及这种模式,是因为在高精度定时场景中,执行任务的时间开销很有可能大于定时器的时间间隔,如果开启新线程执行定时任务,可能会占用大量线程,这个需要结合实际情况考虑如何执行定时任务。这里讨论的是默认在定时器线程上执行定时任务。
https://learn.microsoft.com/en-us/windows/win32/sysinfo/acquiring-high-resolution-time-stamps#low-level-hardware-clock-characteristics ︎
https://learn.microsoft.com/en-us/windows/win32/api/timeapi/nf-timeapi-timebeginperiod?redirectedfrom=MSDN ︎
https://learn.microsoft.com/en-us/previous-versions//dd757634(v=vs.85)?redirectedfrom=MSDN ︎
.NET中如何实现高精度定时器的更多相关文章
- Linux时间子系统之六:高精度定时器(HRTIMER)的原理和实现
转自:http://blog.csdn.net/droidphone/article/details/8074892 上一篇文章,我介绍了传统的低分辨率定时器的实现原理.而随着内核的不断演进,大牛们已 ...
- 高精度定时器实现 z
1背景Permalink .NET Framework 提供了四种定时器,然而其精度都不高(一般情况下 15ms 左右),难以满足一些场景下的需求. 在进行媒体播放.绘制动画.性能分析以及和硬件交互时 ...
- Linux下的hrtimer高精度定时器【转】
转自:http://blog.csdn.net/waverider2012/article/details/38305785 hrtimer高精度定时器的interval由ktime_set(cons ...
- 使用linux内核hrtimer高精度定时器实现GPIO口模拟PWM,【原创】
关键词:Android linux hrtimer 蜂鸣器 等待队列 信号量 字符设备 平台信息:内核:linux3.4.39 系统:android/android5.1平台:S5P4418 作 ...
- linux下jiffies定时器和hrtimer高精度定时器【转】
本文转载自:http://blog.csdn.net/dosculler/article/details/7932315 一.jiffies定时器,HZ=100,精度只能达到10ms. 注:采用jif ...
- iOS中的三种定时器
iOS中的三种定时器 NSTimer 一.背景 定时器是iOS开发中经常使用的,但是使用不慎会造成内存泄露,因为NSTimer没有释放,控制器析构函数dealloc也没有调用,造成内存泄露. 二.使用 ...
- Linux 高精度定时器hrtimer 使用示例【转】
本文转载自:http://blog.csdn.net/dean_gdp/article/details/25481225 hrtimer的基本操作 Linux的传统定时器通过时间轮算法实现(timer ...
- Jmeter—7 测试中使用到的定时器和逻辑控制器
1 测试中提交数据有延时1min,所以查询数据是否提交成功要设置定时器. 固定定时器页面:单位是毫秒 [dinghanhua] 2 集合点.Synchronizing Timer 集合点编辑:集合用户 ...
- C#中WebService 的 Timer定时器过段时间后自动停止运行
我用.net做的一个Timer定时器,定时获取短信并给予回复,但大概过了十几个小时以后,Timer定时器会自动停止,再发送短信就不能收到回复,需要在服务器中重新运行定时器才可以,请教各位! 我是在.n ...
- Java 中Timer和TimerTask 定时器和定时任务使用的例子
转自:http://blog.csdn.net/kalision/article/details/7692796 这两个类使用起来非常方便,可以完成我们对定时器的绝大多数需求 Timer类是用来执行任 ...
随机推荐
- API接口设计规范,看这篇就足以了
优秀的设计是产品变得卓越的原因.设计API意味着提供有效的接口,可以帮助API使用者更好地了解.使用和集成,同时帮助人们有效地维护它.每个产品都需要使用手册,API也不例外. 在API领域,可以将 ...
- iOS发送探针日志到日志系统的简单实现
通过参考Testin的SDK实现方式,我们大致可以确定他们背后的实现方式: 首先,通过加载Testin的SDK,然后收集各种七七八八的数据,再通过socket发送数据到云端. 云端我们已经有了,就是h ...
- DesignPattern-part1
title: "modern C++ DesignPattern-Part1" date: 2018-04-03T16:06:33+08:00 lastmod: 2018-04-0 ...
- VB快速上手文档教程
前言 本来我想可能不会接触到这个语言, 不过在用excel时需要用到VBA. 这就不得不专门去学习一番. 入了个门, 专门写个文档留着. 万一以后用得到呢- 论VB, 我还是初学者. 如有弄错了的地方 ...
- SpringBoot拦截器和动态代理有什么区别?
在 Spring Boot 中,拦截器和动态代理都是用来实现功能增强的,所以在很多时候,有人会认为拦截器的底层是通过动态代理实现的,所以本文就来盘点一下他们两的区别,以及拦截器的底层实现. 1.拦截器 ...
- vue3.3.x setup 新实验性特性 defineModel 定义多个属性
由于有些业务组件需要定义多个响应式props,类似这种(比较懒,没上ts),在vue3.3.x以前,如果不用三方库,代码会变得很繁琐 <script setup> const props ...
- 2023年Vue开发中的8个最佳工具
前言 Vue.js,一款当今非常流行的基于JavaScript的开源框架,旨在构建动态的可交互应用. Vue.js以其直观的语法和灵活的架构而广受全球开发者的欢迎和赞誉.随着时间的推移,Vue不断进化 ...
- oracle-组合索引字段位置与查询效率之间的关系
Oracle索引组合字段的位置不同,当查询条件不能覆盖索引时,影响查询效率.查询条件是不是索引字段的第一列影响执行计划,实验验证 实验1:查询条件为组合索引的第一列--创建测试表 create tab ...
- 文心一言 VS 讯飞星火 VS chatgpt (114)-- 算法导论10.2 7题
七.用go语言,给出一个 O(n)时间的非递归过程,实现对一个含 n个元素的单链表的逆转.要求除存储链表本身所需的空间外,该过程只能使用固定大小的存储空间. 文心一言: 在Go语言中,你可以使用迭代方 ...
- kubeadm 工具部署 kubernetes v1.16.2
环境准备 3个节点,以下基于 Centos 7.6 系统, 内核版本:3.10.0-957.12.2.e17.x86_64 HOST NODE CPU MEM 192.168.1.111 master ...