此贴解决了心里一大疑团,也说明了很多问题,比如高精度定时是否一定起作用了,如果异议,请给出充分理由
众所周知,我们编写的应用程序,或者游戏,作为进程形式运行在系统中,而现代系统为了充分发挥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