慢羊羊的空间

工作做不完了,300出,无瑕。

精确延时的实现 金牌收录

原理讲解

大家平时写练习程序,包括网站上的范例程序,很多延时都直接用的 Sleep() 实现。这个延时有个缺点,那就是无法统计代码执行的时间。请看下图:

由图可以看到,使用 Windows API 函数 Sleep() 的问题,就是会忽略掉程序的执行时间。很多时候,程序的执行时间是不固定的,所以这就导致使用 Sleep 的延时并不精确,即便 Sleep 使用相同的延时,也可能造成不同电脑上执行速度不同的结果。

图中,理想的延时函数会将程序的执行时间部分考虑进去,这样就可以实现很均匀的延时。本次延时要从上次的延时结束开始计算,就必须要记录每次延时执行的具体时刻,而不仅仅是一个时间长度。

并且,Sleep 函数的精度只有 10~16ms 左右,无法完成更精确的延时。

延时函数封装

可以简单的使用 clock() 函数实现精确延时。clock 函数返回程序启动后到函数调用时所经过的时间,单位毫秒,精度能达到 1ms。更保险一点,可以用 CLOCKS_PER_SEC 这个常量,表示每秒钟有多少个 clock 单位量。

封装好的延时函数如下:

// 精确延时函数(可以精确到 1ms,精度 ±1ms)
// by yangw80<yw80@qq.com>, 2011-5-4
void HpSleep(int ms)
{
	static clock_t oldclock = clock();		// 静态变量,记录上一次 tick

	oldclock += ms * CLOCKS_PER_SEC / 1000;	// 更新 tick

	if (clock() > oldclock)					// 如果已经超时,无需延时
		oldclock = clock();
	else
		while(clock() < oldclock)			// 延时
			Sleep(1);						// 释放 CPU 控制权,降低 CPU 占用率,精度 10~16ms
}

直接用函数 HpSleep 替换 Sleep 就可以很直观的看到效果。例如,《自由运动的点》这个程序,就是用的前述函数实现精确延时。

和 clock() 函数类似的还有 GetTickCount() 函数,GetTickCount 函数返回系统启动后到函数调用时所经过的时间,单位毫秒,精度在 10ms~16ms 之间。

以上代码可以实现毫秒级的延时。

如果需要更高的精确度,可以使用多媒体定时器。做为范例,以下代码实现毫秒级的延时,并封装成类。如果有需要,可以修改为微秒级甚至纳秒级的延时。代码如下:

// 代码名称:精确到微秒的延时类(基于多媒体定时器)
// 代码编写:yangw80 <yw80@qq.com>
// 最后修改:2011-5-4
//
#pragma once
#include <windows.h>

class MMTimer
{
private:
	static LARGE_INTEGER m_clk;			// 保存时钟信息
	static LONGLONG m_oldclk;			// 保存开始时钟和结束时钟
	static int m_freq;					// 时钟频率(时钟时间换算率),时间差

public:
	static void Sleep(int ms);
};

LARGE_INTEGER MMTimer::m_clk;
LONGLONG MMTimer::m_oldclk;
int MMTimer::m_freq = 0;

// 延时
void MMTimer::Sleep(int ms)
{
	if (m_oldclk == 0)
	{
		QueryPerformanceFrequency(&m_clk);
		m_freq = (int)m_clk.QuadPart / 1000;	// 获得计数器的时钟频率

		// 开始计时
		QueryPerformanceCounter(&m_clk);
		m_oldclk = m_clk.QuadPart;				// 获得开始时钟
	}

	unsigned int c = ms * m_freq;

	m_oldclk += c;

	QueryPerformanceCounter(&m_clk);

	if (m_clk.QuadPart > m_oldclk)
		m_oldclk = m_clk.QuadPart;
	else
		do
		{
			::Sleep(1);
			QueryPerformanceCounter(&m_clk);	// 获得终止时钟
		}
		while(m_clk.QuadPart < m_oldclk);
}

看明白了前面的叙述,这个代码应该很容易就能看懂。

使用方法:将以上代码拷贝到新建的 MMTimer.h 中,然后在主程序中加上 #include "MMTimer.h",在需要 Sleep 的地方执行 MMTimer::Sleep 方法。

为了简单起见,只写了一个 .h 文件。更标准一些的做法,是将前述代码再分离出一个 MMTimer.cpp 文件,甚至改掉 MMTimer 这个名字,或者封装成库等等,这些就不再多说了,本文只想阐述一个方法。

关于 Sleep 的精度

在延时的循环等待过程中,如果使用空循环,会导致 CPU 占用率很高。通常在循环中加入 Sleep 函数以释放 CPU 占用。

Windows API 函数 Sleep 的精度,通常在 10ms ~ 16ms 左右,可能执行 Sleep(1) 也会延时 18ms,这就造成了延时不太稳定的问题。

如果要求较高,可以通过 timeBeginPeriod 函数临时提升 Sleep 的精度,代码如下:

#include <easyx.h>

#pragma comment(lib,"winmm.lib")		// 1. 链接多媒体库

int main()
{
	timeBeginPeriod(1);					// 2. 临时提升 Sleep 精度为 1ms

	///////////////////////////////////////
	// 其它代码,期间会调用若干次 Sleep() 函数
	///////////////////////////////////////

	timeEndPeriod(1);					// 3. 恢复 Sleep 精度

	return 0;
}

注意,提升 Sleep 精度后,如果是 Win10(2004) 之前的系统,会提升整个系统的 Sleep 精度,影响所有进程,并影响整个系统的性能。所以,务必调用 timeEndPeriod(1),且参数必须和 timeBeginPeriod 一致。从 Win10(2004) 之后,这个设置仅影响当前进程。

调用该设置后,除去函数调用开销,Sleep 精度能达到 2ms 左右。

如果需要再高一些精度,可以直接使用 Sleep(0),无需执行 timeBeginPeriod / timeEndPeriod。Sleep(0) 会让当前线程释放 CPU 时间片给其它待执行的线程,然后立即返回。这样没有过多的延时,会导致 CPU 单核心几乎满载。

总之,延时精度越高,CPU 占用越高,效果也越流畅。

评论 (3) -

  • void HpSleep(int ms)存在帧率长短交替的bug:
    这是由逻辑问题引起的:根据前一帧的持续时间为一帧添加延迟,这意味着在长帧之后,不会应用延迟,从而产生短帧,然后会在下一帧触发延迟,从而产生长帧,循环往复。
  • 感谢楼主的热心帮助。我是凯恩

添加评论