|

楼主 |
发表于 2006-3-14 09:47:35
|
显示全部楼层
多线程理论及性能分析 好,终于到了讨论性能问题的时候了.怎样才能让WINDOWS下的程序运行的更加高效呢?我们需要在原有代码的基础上进行改进 你可能发现从游戏向其它窗口切换在概需要1/3到1/2秒左右的的时间(很慢是吧!).所以我们有必要讲讲多线程以找到正确的方法来改进之 在WINDOWS下一个线程就像一个进程.因为虽然它们共享进程的地址空间,但调度程序却把它们当作独立的进程来处理.什么是调度程序?它是WINDOWS操作系统中十分重要的部分,总管着所有进程的运行.下面我来讲讲调度程序的工作原理吧! 举个例子,如果你的电脑的主频是600MHZ,并且你同时打开了浏览器资源管理器和控制面板3个程序.它们看起来同时运行着,但其实它们不可能真正的同时运行.每个程序在运行时必须独占的访问CPU,CACHE等系统资源.(否则你的程序肯定慢的跟爬一样,J)那么WINDOWS是怎么做的呢?它分给每个程序200MHZ(一个时间片)来运行.就是把一大块可用的时间(600MHZ)分成相等的几个时间片,各个程序在时间片中运行,当一个时间片结束了,就切换到另一个程序,如此循环往复.此时WINDOWS监视着所有打开的程序,所以当你再打开一个程序时(比如说我们的窗口切换程序),就有4个程序打开着了,此时WINDOWS就会重新分配这600MHZ处理器时间,把它分成相等的4份,即每个程序拥有150MHZ的时间片.只要WINDOWS不停止它们,它们就会按照时间片轮流执行.听起来很简单是吧,你懂了就好J总之,WINDOWS中负责安排与调度的这部分就叫做”调度程序”. 尽管如此,调度程序其实很容易被误用.WINDOWS并不知道什么时候你的程序运行,而什么时候你的程序应该暂停.对于WINDOWS来说,你的程序只不过是一个个的字节而已,根本无法说明你的程序要做什么.唯一的解决方案是让你的程序自己来决定它的执行或挂起.所以,问题出现了,因为我们共享着一个处理器,如果你的程序过了时间片还不主动挂起,就会占用其它程序的时间片,从而使它们变的相当地慢.如果你要的话,你可以占着CPU不放,但这会使WINDOWS出问题,并且让用户感觉极为不爽J.我永远也不会忘记那次经历,那次我上网看Diablo游戏策略然后想亲手试试,我只好同时打开着Diablo和那个网页,尽管Diablo是最小化的,我的450MHZ的电脑跑起来还是慢得和我以前那台老爷386一样J我甚至可以用秒来计算它重绘桌面壁纸的时间.游戏程序员们认为在DIRCTX的独占模式下他们可以像对待DOS一样来对待WINDOWS.但是不幸的是,用户们经受着痛苦(慢),而且从其独占模式转到窗口模式又引起很多问题,就像本文介绍的一样. 太多的责备与否定了.现在我们来讨论一下解决此类问题的好的方法!好,让我们先把游戏循环改写成下面这样子:
void GameLoop() { ProcessInput(); ProcessLogic(); ProcessGraphics(); ProcessSound(); }
我们可以做得更好些.因为这里当引擎的一部分非常慢的时候,整个游戏就慢下来了,包括声音,输入等.我们甚至会掉线因为我们用了太多的时间去处理图像而没有很好的检查网络连接.而其它机器也会因为滞后而慢下来.如果数据操作不当或程序设计不好, 玩家都有可能受到影响.(此句原文为: gameplay will suffer because of improper handling of data/poor programming,若译的有错望大家指正) 对此我们有一个简单的方法.我们已经知道有些处理必须实时运行,否则游戏就没有意义了. ProcessInput()就是这样一个处理,它处理键盘,鼠标的输入(包括多人输入),但是它和又其它功能紧紧的联系在一起(见以上GameLoop).怎么把它分离出来呢?对,我们把它放到另外一个线程中.那么什么是线程呢?它是程序在运行中产生的新的处理单位(你可以把它看成下蛋J),WINDOWS调度程序像处理独立的程序一样来处理它.但线程却又独立于产生它的进程运行.线程可以访问该程序的地址空间,当游戏循环在执行时,线程在后台运行. 那么怎样创建一个线程呢?关于这个话题另外已有一篇文章<< Separating Input from the Game Loop >>详细的介绍了这些内容.在这里作为补充来说说如何给你创建的线程分配优先级. 优先级使我们的问题复杂化,但在我看来,它所带来的好处超过它带来的复杂性.首先,这不再是一个普通的时间片,每个程序(线程)都有自己的优先级.不同的程序具有不同的优先级,举个例子,设备驱动程序可能会创建一个高优先级的线程,它首先把自己的时间让给其它线程,当它具备了所需的条件时,它就可以完全的控制系统了.当然,这也只不过是几毫秒或更少的时间,所以不会怎么影响系统.但它必须实时执行,所以当它运行时,它必须独占系统;对于像WORD这样的程序,可能会创建一个低优先级的线程来做后台打印工作.这样,只有在系统中没有其它程序运行时才会执行后台打印线程.这并不会影响你,因为当你运行其它一般的或高优先级的程序时,它就会立即挂起.所以线程优先级是一种灵活使用CPU的方法.你告诉WINDOWS你要做什么,WINDOWS就相应的为你安排好一切J 我们如何告诉WINDOWS何时要切换到另一个程序?你可以调用Sleep(0)函数.但是为什么我们的消息循环中没有这个语句,至少游戏循环中就没有?记住了, GetMessage() 其实就是SleepUntilIHaveAMessage() 但是如果你的游戏在窗口模式下,你就必须用到线程了.如果你对我之前所讲的有点头绪的话,你就可能会问:我怎样才能使用一个线程呢?好的,你首先要做的就是编写 ThreadProcedure函数,它看来有点像WinMain函数,其一般形式如下:
#define WM_THREADSTOP (WM_USER+1)
DWORD WINAPI ThreadProcedure(LPVOID lpStartupParam) { // get the data we passed to the thread. Note that we don't have to use this // at all if we don't want MYDATA* pMyData = (MYDATA*) lpStartupParam;
// access some imaginary members of MYDATA, which you can define on // your own later pMyData->nTime = GetCurrentTime(); // imaginary function I created pMyData->nNumber = 5;
// here's the thread's main loop ?kind of like the main loop in WinMain MSG msg;
for( ;; ) { if( PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE) { GetMessage(&msg, NULL, 0, 0);
if( msg.message == WM_THREADSTOP ) break; // only way out of the for( ;; ) loop
TranslateMessage(&msg); DispatchMessage(&msg); } else { // do the task ?add in your own stuff here
// yield to other threads, because we almost never get messages // (note that we may be yielding to WinMain too) Sleep(0); } } }
注意两个要点1)线程几乎不接收消息,除非它创建了自身的窗口,对,线程可以创建并处理其自身的窗口.(想想为什么WORD2000会用起来这么爽J).但是,如果一个线程不具有自身的窗口,它不太可能会接收消息,所以我们用Sleep(0)来通知WINDOWS现在是时候把时间片让给其它线程了.如果我们不这么做, 我们和程序和主窗口就会变得特别慢,而这是不必要的.2)因为我们几乎不可能执行到PeekMessage()-GetMessage()-DispatchMessage()这段程序,所以消息处理的代码并不会降低我们程序的执行速度. 那么我为什么要加入消息循环呢(既然不会执行到它),如果我不创建线程的窗口,这不是多余吗!不,我这样做是因为这样我们就可以向线程发送我们自定义的消息了.在主程序退出前,我们必须通知线程停止执行并释放资源,这就是我为什么要自定义一个消息了,你可以把它用PostThreadMessage()把它发送给线程.注意你也必须等到线程结束(再结束主程序),所以你还要一个全局变量来判断线程是否结束:
int g_nThreadExitCount = 0;
只有当线程从主消息循环中退出时,才自增此变量并返回0;当此变量增加到你运行的线程数时,说明你所有的线程都已停止,你就可以安全的退出WinMain了.千万别用 TerminateThread函数(它不能很好的释放资源).
|
|