定时器:为 Windows 实现一个连续更新,高精度的时间供应器
你为什么会对获得小于1毫秒精度的系统时间感兴趣?在我工作期间,我发现有必要去确定我的进程里不同线程执行引发的事件的顺序。还需要把这些事件同绝对时间相 关联,但注意到系统时间的实际精度是不会超过10毫秒粒度的。 在本文随后的内容中,我将解释该系统时间精度的限制,解决的步骤,以及某些一般缺陷。例子程序的实现可以从本文开始链接处下载。这些文件的源代码是在 Visual C++? 7.1 和 Windows? XP 专业版下编写测试的。在编写本文时,我频繁地提到 Windows NT® 操作系统家族(Windows NT 4.0, Windows 2000, 或者 Windows XP)产品,而不是某一个特定的版本。 本文中用到的 Win32? APIs 的参数类型及用法,参见 MSDN library/Platform SDK 文档。 究竟谁有这样的需求? 最近我用“Windows NT millisecond time resolution”作为关键字在 Internet 上搜索了一番, 得到了 400 多个满足条件的结果。其中大多数是讨论如何获得高于10毫秒精度的系统时间,或者是如何让一个线程的休眠时间小于10毫秒。本文我将专注于为什么获得一个高于10毫秒精度的系统时间 会如此困难。你可能认为用 GetSystemTime API 很容易解决问题,这个 API 函数返回一个SYSTEMTIME 结构,该结构包含一个 wMilliseconds 域,在 MSDN 文档中说它保存 当前的毫秒时间。但实际上并不象这么简单。那么用 GetSystemTimeAsFileTime 获取 100 纳秒的精度如何呢?就让我们从一个小试验 开始吧:尝试重复获取系统时间,将它格式化并输出到屏幕上(见 Figure 1 )。 我的目标不是纳秒,而仅是毫秒精度,它应该能够从 SYSTEMTIME 结构中判断。让我们看一下输出结果: 20:12:23.479 20:12:23.479 20:12:23.494 20:12:23.494 [...有很多被移去了...] 20:12:23.494 20:12:23.509 20:12:23.509 20:12:23.509 ...正如你所看到的,我所能得到的最好的精度是15毫秒,这是 Windows NT 时钟周期的长度。每过一个时钟周期,Windows NT都会更新系统时间。Windows NT调度器也会 突然启动并可能选择一个新的线程来执行。关于这方面的更多信息,请看《Inside Windows 2000》第三版(Microsoft Press®, 2000),作者是 David Solomon 和 Mark Russinovich。 如果你运行我刚才所示的代码,你也许会看到时间大约是每10毫秒更新一次。如果是那样,可能意味着你是在单处理器的机器上运行 Windows NT,其时钟周期通常为10毫秒。正如你所看到的, 在这种方法中,系统时间更新频率不够快,不足以成为一种为我所用的技术。下面我们就来尝试找一个解决方案。 最初的尝试 当你询问如何得到一个比10毫秒精度更好的系统时间时,你也许会得到下面这样的回答:使用性能计数器,并让性能计数器值和即时变化的系统时间同步。结合这些值来计算一个 精度极高的当前时间。Figure 2 显示了实现方法。 性能计数器是一个高精度的硬件计数器,它能高精确、低开销地计量一个短周期时间。我通过在一个紧凑循环内不断重复把性能计数器值和对应的系统时间进行同步,等待系统时间变化。当系统时间 以变,我就保存计数器的值及系统时间。 使用这两个值作为参考,就有可能计算出一个高精度的当前系统时间(详情见 Figure 2 中的get_time),看一下结果: ... 21:23:22.296 21:23:22.297 21:23:22.297 21:23:22.298 21:23:22.298 21:23:22.299 21:23:22.300 21:23:22.300 21:23:22.301 21:23:22.301 21:23:22.302 21:23:22.302 21:23:22.303 ...尽管它看起来非常成功,但这个实现却有几个问题:同步实现(函数被命名为 "simplistic_synchronize"的一个很好的理由);QueryPerformanceFrequency 报告的频率 ;系统时间变化缺乏保护。在接下来的章节中,我们会考虑这些问题的一些可能的改进。 实现同步的可靠方法 该同步实现没有考虑 Windows NT 调度器的抢先问题。例如,它无法保证在下面的两行代码之间不会发生线程上下文的切换,从而导致一个未知时间周期的延迟: ::GetSystemTimeAsFileTime(&ft1); ::QueryPerformanceCounter(&li);大多时候只要满足下面的条件,这个过分单纯化的同步函数还是成功的:
|
作者简介 Johan Nilsson是在 Esrange 的瑞士空间公司的一个系统工程师,位于北极圈之上。自从Windows NT 4.0发布以来他就一直使用C++为Windows NT开发软件,从Windows 3.1起为Windows/DOS编程。和他联系:johan.nilsson@esrange.ssc.se |
本文由 VCKBASE MTT 翻译
#include <windows.h>
#include <iostream>
#include <iomanip>
int main(int argc, char* argv[])
{
SYSTEMTIME st;
while (true)
{
::GetSystemTime(&st);
std::cout << std::setw(2) << st.wHour << ':'
<< std::setw(2) << st.wMinute << ':'
<< std::setw(2) << st.wSecond << '.'
<< std::setw(3) << st.wMilliseconds << '/n';
}
return 0;
}
#include <windows.h>
#include <iostream>
#include <iomanip>
struct reference_point
{
FILETIME file_time;
LARGE_INTEGER counter;
};
void simplistic_synchronize(reference_point& ref_point)
{
FILETIME ft0 = {0, 0},
ft1 = {0, 0};
LARGE_INTEGER li;
//
// Spin waiting for a change in system time. Get the matching
// performace counter value for that time.
//
::GetSystemTimeAsFileTime(&ft0);
do
{
::GetSystemTimeAsFileTime(&ft1);
::QueryPerformanceCounter(&li);
}
while((ft0.dwHighDateTime == ft1.dwHighDateTime) &&
(ft0.dwLowDateTime == ft1.dwLowDateTime));
ref_point.file_time = ft1;
ref_point.counter = li;
}
void get_time(LARGE_INTEGER frequency, const reference_point&
reference, FILETIME& current_time)
{
LARGE_INTEGER li;
::QueryPerformanceCounter(&li);
//
// Calculate performance counter ticks elapsed
//
LARGE_INTEGER ticks_elapsed;
ticks_elapsed.QuadPart = li.QuadPart -
reference.counter.QuadPart;
//
// Translate to 100-nanosecondsintervals (FILETIME
// resolution) and add to
// reference FILETIME to get current FILETIME.
//
ULARGE_INTEGER filetime_ticks,
filetime_ref_as_ul;
filetime_ticks.QuadPart =
(ULONGLONG)((((double)ticks_elapsed.QuadPart/(double)
frequency.QuadPart)*10000000.0)+0.5);
filetime_ref_as_ul.HighPart = reference.file_time.dwHighDateTime;
filetime_ref_as_ul.LowPart = reference.file_time.dwLowDateTime;
filetime_ref_as_ul.QuadPart += filetime_ticks.QuadPart;
//
// Copy to result
//
current_time.dwHighDateTime = filetime_ref_as_ul.HighPart;
current_time.dwLowDateTime = filetime_ref_as_ul.LowPart;
}
int main(int argc, char* argv[])
{
reference_point ref_point;
LARGE_INTEGER frequency;
FILETIME file_time;
SYSTEMTIME system_time;
::QueryPerformanceFrequency(&frequency);
simplistic_synchronize(ref_point);
while (true)
{
get_time(frequency, ref_point, file_time);
::FileTimeToSystemTime(&file_time, &system_time);
std::cout << std::setw(2) << system_time.wHour << ':'
<< std::setw(2) << system_time.wMinute << ':'
<< std::setw(2) << system_time.wSecond << ':'
<< std::setw(3) << system_time.wMilliseconds << '/n';
}
return 0;
}
模板参数 |
---|
counter_type 代表高精度,高频率的计数器。它必须提供静态成员值和频率,同value_type定义一样。 KEEP_WITHIN_MICROS 定义时间供应器最大可以偏离实际系统时间的微秒个数。它也影响再同步线程的同步频率。 SYNCHRONIZE_THREAD_PRIORITY 定义同步线程在执行同步时应该设置的自身优先级。这个不应该被修改除非你的程序不断的在一个高优先级上执行。缺省的是THREAD_PRIORITY_BELOW_NORMAL,这样不会打扰正常或高优先级线程的正常执行。 TUNING_LIMIT_PARTSPERBILLION 当前时间供应器的实现是连续的测量计数器频率。这个频率在内部被维护,允许较少频率的再同步和更准确的定时。当测量的频率的精确度达到一定阈值时,就不会再执行调整(但周期性再同步总是活动的)。这个极限的单位是计算频率的错误比率,对应的缺省值是每10亿100单位。 MAX_WAIT_MILLIS 定义允许的最大调谐间隔,毫秒为单位——也就是,检查高精度时间偏离系统时间有多远前的等待时间。调谐间隔是自动调整的,但只能达到这个极限。这个参数一般不应该被修改。 MIN_WAIT_MILLIS 定义最小允许的调谐间隔,毫秒为单位。细节见MAX_WAIT_MILLS |
类型定义 |
raw_value_type 能够存储“原始”时戳的类型 |
成员函数 |
instance 返回这个类的唯一实例的引用 systemtime返回当前的系统时间,格式是SYSTEMTIME结构 filetime 返回当前系统时间,格式是FILETIME结构 rawtime 返回当前系统时间,用最小的负荷返回“原始”时戳。为了把它转为绝对时间使用filetime_from_rawtime或者systemtime_from_rawtime systemtime_from_rawtime 把“原始”时戳转为绝对时间,用SYSTEMTIME结构表示 filetime_from_rawtime 把“原始”时戳转为绝对时间,用FILETIME结构表示 |
#include <hrt/performance_counter.hpp>
#include <hrt/time_provider.hpp>
#include <hrt/system_time.hpp>
#include <vector>
#include <iostream>
#include <iomanip>
using namespace hrt;
typedef time_provider<performance_counter> time_provider_type;
typedef time_provider_type::raw_value_type raw_time_type;
typedef std::vector<raw_time_type> raw_vector;
const int NUMBER_OF_SAMPLES = 1000;
int main(int argc, char* argv[])
{
raw_vector samples;
time_provider_type& provider = time_provider_type::instance();
samples.reserve(NUMBER_OF_SAMPLES);
for (int i = 0; i < NUMBER_OF_SAMPLES; ++i)
{
samples.push_back(provider.rawtime());
}
system_time st;
for (raw_vector::iterator iter = samples.begin();
iter != samples.end(); ++iter)
{
provider.systemtime_from_rawtime(*iter, st.pointer());
std::cout << std::setfill('0')
<< std::setw(2) << st.hour() << ':'
<< std::setw(2) << st.minute() << ':'
<< std::setw(2) << st.second() << '.'
<< std::setw(3) << st.millis() << '/n';
}
return 0;
}
Win32 API | 执行时间 | time_provider | 执行时间 |
---|---|---|---|
GetSystemTimeAsFileTime | 1.9% (~0%) | filetime | 135% (900%) |
GetSystemTime | 100% (100%) | systemtime | 234% (1001%) |
QueryPerformanceCounter | 55% (400%) | rawtime | 57% (400%) |
同步:有多好?
使用我在文中描述的同步方法,你可以指定你想要的结果精度。然而,实际上,你能得到的结果的质量有平台相关性(硬件和软件)限制。在 Windows NT 中时钟中断处理器需要花费时间来执行,大大地限制了你的精度不可能优于时钟中断处理器的执行时间,加上线程上下文切换时间,还有当时间变化时调用函数进行检查所花的时间。如果你在对称多处理(SMP)机器上运行,你可以通过在另一个 CPU 上运行同步线程来避免时钟中断问题。
在 SMP 机器上禁止同步线程运行在处理时钟中断的 CPU 上可以产生数十倍差异的同步精度。唯一的问题是你要首先知道哪个 CPU 在处理实际的时钟中断。从我有限的经验来看我只能告诉你好像是CPU#0来处理(我想这种感觉有些怪怪的)。假设这是真的,你可以仅仅使用 SetThreadAffinityMask API 从允许处理器的线程列表中移去 CPU#0。你应该通过预先检查 GetProcessAffinityMask 的调用结果来确认该进程被允许在另一个处理器上运行。
http://blog.csdn.net/jiangxinyu/article/details/2728416