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

2019-12-30 12:21:47丽君

自旋等待:让 CPU 空转消耗时间,占用大量 CPU 时间,但是时间高度可控。
阻塞等待:线程进入阻塞状态,出让 CPU 时间片,在等待一定时间后再由操作系统调度回到运行状态。阻塞时不占用 CPU,然而需要操作系统调度,时间难以控制。
可以看到二者各有优劣,应该按照不同需求进行不同的实现。

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

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

2.1自旋等待

可以使用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阻塞等待

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