你為什么會對獲得小於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