论时间片与定时器

    此贴解决了心里一大疑团,也说明了很多问题,比如高精度定时是否一定起作用了,如果异议,请给出充分理由
    众所周知,我们编写的应用程序,或者游戏,作为进程形式运行在系统中,而现代系统为了充分发挥cpu的作用,采用了时间片造成程序并行运行的假象。当然如果有多核的话,也能实现一部分并行计算,不过主要还是靠分时间片运行程序。这就带来了一个问题:如何让程序在指定时间做某件事。这不是一个容易回答的问题,因为该程序在正常情况下通常需要先获得cpu时间片,才可以做一会自己的工作,时间片用完后该进程返回就绪队列等待cpu下次调度。那么这就会有一个问题,在应用层程序设计中,如果指定时间到了,而时间片恰好没有轮到该进程,会怎样呢,程序是否能真正实现准确定时?笔者认为答案是否定的,下面这些采用已知定时器函数足以说明问题:

[C++] 纯文本查看 复制代码
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为例):

[C++] 纯文本查看 复制代码
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

[C++] 纯文本查看 复制代码
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

posted @ 2016-06-03 18:03  findumars  Views(2248)  Comments(0Edit  收藏  举报