此貼解決了心里一大疑團,也說明了很多問題,比如高精度定時是否一定起作用了,如果異議,請給出充分理由
眾所周知,我們編寫的應用程序,或者游戲,作為進程形式運行在系統中,而現代系統為了充分發揮cpu的作用,采用了時間片造成程序並行運行的假象。當然如果有多核的話,也能實現一部分並行計算,不過主要還是靠分時間片運行程序。這就帶來了一個問題:如何讓程序在指定時間做某件事。這不是一個容易回答的問題,因為該程序在正常情況下通常需要先獲得cpu時間片,才可以做一會自己的工作,時間片用完后該進程返回就緒隊列等待cpu下次調度。那么這就會有一個問題,在應用層程序設計中,如果指定時間到了,而時間片恰好沒有輪到該進程,會怎樣呢,程序是否能真正實現准確定時?筆者認為答案是否定的,下面這些采用已知定時器函數足以說明問題:
01 |
#include <windows.h> |
02 |
#include <stdio.h> |
03 |
04 |
HINSTANCE hinst; |
05 |
int WINAPI WinMain( HINSTANCE , HINSTANCE , LPSTR , int ); |
06 |
BOOL InitApplication( HINSTANCE ); |
07 |
BOOL InitInstance( HINSTANCE , int ); |
08 |
LRESULT CALLBACK MainWndProc( HWND , UINT , WPARAM , LPARAM ); |
09 |
10 |
#define IDT_TIMER1 10 |
11 |
#define IDT_TIMER2 20 |
12 |
#define IDT_TIMER3 30 |
13 |
14 |
static int count1=1; |
15 |
static int count2=1; |
16 |
static HWND mainwnd; |
17 |
18 |
int WINAPI WinMain( HINSTANCE hinstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) |
19 |
{ |
20 |
MSG msg; |
21 |
if (!InitApplication(hinstance)) |
22 |
return FALSE; |
23 |
if (!InitInstance(hinstance, nCmdShow)) |
24 |
return FALSE; |
25 |
BOOL fGotMessage; |
26 |
27 |
SetTimer(mainwnd,IDT_TIMER1,14,NULL); |
28 |
SetTimer(mainwnd,IDT_TIMER2,15,NULL); |
29 |
SetTimer(mainwnd,IDT_TIMER3,100,NULL); |
30 |
31 |
while (GetMessage(&msg, ( HWND ) NULL, 0, 0)) |
32 |
{ |
33 |
TranslateMessage(&msg); |
34 |
DispatchMessage(&msg); |
35 |
} |
36 |
return msg.wParam; |
37 |
UNREFERENCED_PARAMETER(lpCmdLine); |
38 |
} |
39 |
40 |
BOOL InitApplication( HINSTANCE hinstance) |
41 |
{ |
42 |
WNDCLASSEX wcx; |
43 |
ZeroMemory(&wcx, sizeof (wcx)); |
44 |
wcx.cbSize = sizeof (wcx); |
45 |
wcx.style = CS_HREDRAW | CS_VREDRAW; |
46 |
wcx.lpfnWndProc = MainWndProc; |
47 |
wcx.hInstance = hinstance; |
48 |
wcx.hIcon = LoadIcon(NULL, IDI_APPLICATION); |
49 |
wcx.hCursor = LoadCursor(NULL, IDC_ARROW); |
50 |
wcx.hbrBackground = ( HBRUSH )GetStockObject( WHITE_BRUSH); |
51 |
wcx.lpszMenuName = "MainMenu" ; |
52 |
wcx.lpszClassName = "MainWClass" ; |
53 |
wcx.hIconSm = ( HICON )LoadImage(hinstance,MAKEINTRESOURCE(5),IMAGE_ICON, GetSystemMetrics(SM_CXSMICON), |
54 |
GetSystemMetrics(SM_CYSMICON), LR_DEFAULTCOLOR); |
55 |
return RegisterClassEx(&wcx); |
56 |
} |
57 |
58 |
BOOL InitInstance( HINSTANCE hinstance, int nCmdShow) |
59 |
{ |
60 |
HWND hwnd; |
61 |
hinst = hinstance; |
62 |
hwnd = CreateWindow( "MainWClass" , "Sample" ,WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT, |
63 |
CW_USEDEFAULT,( HWND ) NULL,( HMENU ) NULL,hinstance,( LPVOID ) NULL); |
64 |
if (!hwnd) |
65 |
return FALSE; |
66 |
mainwnd=hwnd; |
67 |
ShowWindow(hwnd, nCmdShow); |
68 |
UpdateWindow(hwnd); |
69 |
return TRUE; |
70 |
} |
71 |
72 |
LRESULT CALLBACK MainWndProc( HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) |
73 |
{ |
74 |
if (uMsg == WM_TIMER) |
75 |
{ |
76 |
if (wParam == IDT_TIMER1) |
77 |
{ |
78 |
count1++; |
79 |
return 0; |
80 |
} |
81 |
else if (wParam == IDT_TIMER2) |
82 |
{ |
83 |
count2++; |
84 |
return 0; |
85 |
} |
86 |
else if (wParam == IDT_TIMER3) |
87 |
{ |
88 |
char str[20]; |
89 |
sprintf_s(str, "%d-%d\n" ,count1,count2); |
90 |
SetWindowText(hWnd,str); |
91 |
return 0; |
92 |
} |
93 |
} |
94 |
return DefWindowProc(hWnd,uMsg,wParam,lParam); |
95 |
} |
以上程序在我機子上測試,發現時間間隔調整為14 15時才有變化。這個是利用WM_TIMER消息進行定時,因此有消息傳播時延,不過還是可以在一定程序上得出結論,SetTimer一定有個最小間隔,其內核函數對應NtUserSetTimer,如果傳入時間<15ms,那么間隔就設為15ms(具體請參閱ReactOS源碼)。這個最小間隔一定是基於時間片長度的考量,而windows時間片據說是10-20ms之間的。
其它函數的測試,為了減少冗余指令和時間片和其他因素的影響,我采取了一種特殊方式並提供了模板如下(以SleepEx為例):
01 |
#include <windows.h> |
02 |
#include <stdio.h> |
03 |
static int i=0; |
04 |
static int j=0; |
05 |
06 |
DWORD WINAPI mythread1( LPVOID ) |
07 |
{ |
08 |
while ( true ) |
09 |
{ |
10 |
SleepEx(1,FALSE); |
11 |
i++; |
12 |
printf ( "i=%d\n" ,i); |
13 |
} |
14 |
} |
15 |
16 |
DWORD WINAPI mythread2( LPVOID ) |
17 |
{ |
18 |
while ( true ) |
19 |
{ |
20 |
SleepEx(10,FALSE); |
21 |
j++; |
22 |
printf ( "j=%d\n" ,j); |
23 |
} |
24 |
} |
25 |
26 |
int main() |
27 |
{ |
28 |
HANDLE hthread[2]; |
29 |
hthread[0]=CreateThread(NULL,0,mythread1,NULL,0,NULL); |
30 |
hthread[1]=CreateThread(NULL,0,mythread2,NULL,0,NULL); |
31 |
WaitForMultipleObjects(2,hthread,TRUE,INFINITE); |
32 |
CloseHandle(hthread[0]); |
33 |
CloseHandle(hthread[1]); |
34 |
} |
i=23973
j=13963
i=23974
i=23975
j=13964
i=23976
i=23977
j=13965
i=23978
i=23979
j=13966
i=23980
i=23981
j=13967
i=23982
i=23983
j=13968
i=23984
j=13969
i=23985
j=13970
i=23986
i=23987
1GHZ的cpu,一般1s可以執行上億條指令,而已知win的時間片為10ms-20ms,因此需要謹慎選擇對比的時間間隔,使要考察的執行時間遠大於無關冗余代碼執行時間,而又能分辨出時間片,我選擇的間隔為5ms和10ms,結果如上。可見隨着遞增,i和j的倍數關系甚至小於2,而本來這個倍數應該接近10的。足以說明時間片是有影響的,而使定時不夠精確。其他代碼我都有測試,結果相同:GetTickCount,timeGetTime
001 |
#include <windows.h> |
002 |
#include <stdio.h> |
003 |
static int i=0; |
004 |
static int j=0; |
005 |
006 |
DWORD WINAPI mythread1( LPVOID ) |
007 |
{ |
008 |
while ( true ) |
009 |
{ |
010 |
DWORD dwStart=GetTickCount(); |
011 |
DWORD dwEnd=dwStart; |
012 |
do |
013 |
{ |
014 |
dwEnd=GetTickCount()-dwStart; |
015 |
} |
016 |
while (dwEnd<5); |
017 |
018 |
i++; |
019 |
printf ( "i=%d\n" ,i); |
020 |
} |
021 |
} |
022 |
023 |
DWORD WINAPI mythread2( LPVOID ) |
024 |
{ |
025 |
while ( true ) |
026 |
{ |
027 |
DWORD dwStart=GetTickCount(); |
028 |
DWORD dwEnd=dwStart; |
029 |
do |
030 |
{ |
031 |
dwEnd=GetTickCount()-dwStart; |
032 |
} |
033 |
while (dwEnd<10); |
034 |
035 |
j++; |
036 |
printf ( "j=%d\n" ,j); |
037 |
} |
038 |
} |
039 |
040 |
int main() |
041 |
{ |
042 |
HANDLE hthread[2]; |
043 |
hthread[0]=CreateThread(NULL,0,mythread1,NULL,0,NULL); |
044 |
hthread[1]=CreateThread(NULL,0,mythread2,NULL,0,NULL); |
045 |
WaitForMultipleObjects(2,hthread,TRUE,INFINITE); |
046 |
CloseHandle(hthread[0]); |
047 |
CloseHandle(hthread[1]); |
048 |
049 |
} |
050 |
051 |
052 |
#include <windows.h> |
053 |
#include <stdio.h> |
054 |
#pragma comment(lib,"winmm.lib") |
055 |
static int i=0; |
056 |
static int j=0; |
057 |
058 |
DWORD WINAPI mythread1( LPVOID ) |
059 |
{ |
060 |
while ( true ) |
061 |
{ |
062 |
DWORD dwStart=timeGetTime(); |
063 |
DWORD dwEnd=dwStart; |
064 |
do |
065 |
{ |
066 |
dwEnd=timeGetTime()-dwStart; |
067 |
} |
068 |
while (dwEnd<10); |
069 |
070 |
i++; |
071 |
printf ( "i=%d\n" ,i); |
072 |
} |
073 |
} |
074 |
075 |
DWORD WINAPI mythread2( LPVOID ) |
076 |
{ |
077 |
while ( true ) |
078 |
{ |
079 |
DWORD dwStart=timeGetTime(); |
080 |
DWORD dwEnd=dwStart; |
081 |
do |
082 |
{ |
083 |
dwEnd=timeGetTime()-dwStart; |
084 |
} |
085 |
while (dwEnd<20); |
086 |
087 |
j++; |
088 |
printf ( "j=%d\n" ,j); |
089 |
} |
090 |
} |
091 |
092 |
int main() |
093 |
{ |
094 |
HANDLE hthread[2]; |
095 |
hthread[0]=CreateThread(NULL,0,mythread1,NULL,0,NULL); |
096 |
hthread[1]=CreateThread(NULL,0,mythread2,NULL,0,NULL); |
097 |
WaitForMultipleObjects(2,hthread,TRUE,INFINITE); |
098 |
CloseHandle(hthread[0]); |
099 |
CloseHandle(hthread[1]); |
100 |
101 |
} |
如果不能實現如此精准的定時,那么我覺得在游戲中采用如此精准的時間是否有實際用途就不得而知了。當然這是僅僅考慮除cpu定時器的情況下得出的,如果采用其他設備的定時器結果如何筆者還不得而知,不過筆者認為,只要程序遵循時間片,且不強制剝奪時間片,那么必然還是無法達到精確時間。除非此執行代碼段獨立於進程且作為定時回調,是否有這種情況,還需要大家廣開言路了。當然這不是說無法獲取到精確時間,顯然精確時間可以獲取到,只是無法做到精確定時,在這種時候,獲取到精確時間作用也不是很大。如果以上分析有誤或有更好的解釋和證明,請給出。
對於一款游戲,它確實需要精確的定時,其實就是在每一幀的開頭取得當前幀開始的時間,然后通過不同幀時間之間相減取得每幀渲染所消耗的時間(包括垂直同步)。因此只有取得了精確的時間才能取得正確的彈性時間,來作為物理計算的參數。如果取得的時間以毫秒為單位的話,假設在一種沒有打開垂直同步的渲染模式下渲染游戲,顯卡比較好,那么FPS值可能會到達很高的水平,彈性時間計算出來可能會變成一個非常小的正值甚至是零,甚至小於零,導致物理引擎崩潰(FPU除零等問題)
此外,有些游戲是音樂游戲,比如OSU,歌姬計划,太鼓達人,DjMax,勁舞團(及其山寨版本“QQ炫舞”),還有安卓上的Deemo、節操大師等,這些游戲考驗的是玩家的節奏感,也就是玩家要跟着節拍和譜面進行“演奏”,評分的標准是節拍是否打得精准,反應是否夠快,按鍵是否正確等。因此這些游戲對“取得時間”這個操作的精度的要求非常高。我不是音樂人但是我也知道作為一個音樂人,他應該對節奏和時間的把握是很精確的。
https://www.0xaa55.com/forum.php?mod=viewthread&tid=978&extra=page%3D11