Multi-threading Theory and Performance IssuesWell, we finally get to the performance stuff. How do we make the game run more efficiently in Windows? There are still a few things to improve on with the code I gave you in Part 1. First off, you may have noticed that it takes about 1/3 or 1/2 of a second to stop the game when you change to another window. We must talk a little about multi-threading topics to understand how we can correct this problem. A thread, in Windows, is almost like another program. It uses the same data space, but the scheduler treats it just like another program. What is the scheduler? It’s a very important part of Windows that determines how programs run. Let me explain how the scheduler works now. Just for example, say you have a 600 MHz processor. Say you’ve got your web browser open, Windows Explorer, and maybe Control Panel. Those 3 programs are running at the same time, but they can’t run at the exact same time. It just isn’t possible. Each program has to have exclusive access to the CPU and cache, etc. while it is running (otherwise, your programs would slow to a crawl). So, how does Windows do it? It gives each of those programs 200 MHz (a time-slice) to run. So, what it does is it divides the amount of time available (600 MHz, here) into equal slices, and allows each program to run for that time-slice, then it switches to the next program, and on and on... All the while, Windows is keeping track of the amount of programs open, so when you start a new program up (let’s say my Mode Switch Demo), that makes 4 programs that are running. Windows sees this and re-divides that 600 MHz of processor time into 4 equal portions, coming up with 150 MHz for each program. So, each program now gets to execute for 150 MHz, before Windows pauses it and lets the next program run for 150 MHz, and so on... This is a little simplistic, but you get the idea. ;-) Anyway, the part of Windows that does all this juggling of programs is called the scheduler. However, the scheduler can easily be misused. Windows can’t tell when your program must execute and when it’s okay for your program to be paused. All of your program code is just bytes to Windows – it can’t possibly tell what you are trying to do. The only feasible solution is for Windows to let your program run until your program decides it is okay to pause for a little while. After all, you are sharing the processor. If you don’t relinquish your time-slice to Windows for use by another program, other programs don’t get enough time to execute, and that means they slow down considerably. You can hog the CPU if you want, but that will cause problems with Windows, and make it extremely frustrating for the user. I’ll never forget when I was reading a strategy guide online for Diablo and went to try something complicated. I had to keep looking at the web page and going back to Diablo. It took forever to browse the web page with Diablo open, even though Diablo was minimized. My 450 MHz computer ran like an old 386 I once had. I could measure the time it took to redraw just the desktop wallpaper in seconds. Programmers typically feel that they can treat Windows like DOS when they’re in DX exclusive mode. Unfortunately, the users suffer. And porting to windowed mode introduces many problems into that way of thinking. Enough negativism and lectures; let’s talk about all the neat solutions to these problems! Okay, instead of the typical game loop, which looks like this:
...we can do something a bit more elegant. For example, if one part of the engine is incredibly slow, the whole game slows down, including sound, input, etc. We may lose the network or Internet connection because we spend too much time processing graphics that we don’t check the network connection often enough. Other machines could slow down because of the "lag", or gameplay will suffer because of improper handling of data/poor programming. There is an easy solution to this, however. We already know that certain things must run in real-time, or the game is useless. ProcessInput() handles keyboard/mouse/multiplayer input, but it is tied down to all the other functions. How do we separate it? Well, we put it in another thread! What’s a thread? Well, it’s kind of like spawning a separate program at run-time, which Windows will proceed to schedule in the same manner, but the thread runs independently of the process. However, the thread does have access to the data in the program! In other words, the thread runs in the background while the game loop is running. How do I make a thread? Well, there is another article on these same topics, called Separating Input from the Game Loop, which explains this rather thoroughly. Suffice it to say that you can assign priorities to the threads you create. Priorities introduce a few more complications to the matter, but in my opinion, the benefits far outweigh the complications. First, there is not only a general time-slice, but also a priority for each program (and each thread). Different programs can have different priorities. Device drivers, for example, might create a high-priority thread that spends most of its time relinquishing its time to other threads. When something needs done, however, that device driver thread now has complete freedom to hog the system. Usually, this is for a few milliseconds or an even smaller time, so it’s almost unnoticeable. But it’s imperative that the task be done in real-time, so, when it executes, it must hog the system. A program, like Microsoft Word, might create a low-priority thread to do background printing. That way, that background printing thread gets to run when nothing else in the system is going on, which makes it unnoticeable to you because it yields whenever a normal- or high-priority thread needs to do something. So, thread priority is a kind of negotiation of how to use the CPU. You tell Windows what you have to do, and Windows schedules everything accordingly. How do you tell Windows when it’s okay to yield to another program? You call the Sleep(0) function. But, how come we don’t have a sleep statement in the main message loop? Or at least in the game loop? Remember, GetMessage() is like SleepUntilIHaveAMessage(). ;-) But, if you’re going to do games in windowed mode, you have to know about threads. If you understood at least some of the preceding babble, you’ll be asking "How can I create threads?" Well, the first thing you do is code the ThreadProcedure, which is kind of like a WinMain. A typical ThreadProcedure looks like this:
There are two important things to remember. 1) Threads almost never get messages, unless they create their own windows. Yes, threads can create and process their own windows. (Look at Microsoft Word 2000 for an example of how this can be used effectively) But, if a thread does not have a window, it will most likely never get messages, so we use the Sleep(0); statement to tell Windows when it is okay to yield to other threads and programs. If we do not do this, our program and its main window become sluggish, which is unnecessary. 2) Since it almost never gets to execute the PeekMessage()-GetMessage()-DispatchMessage() stuff, the message code stuff doesn’t slow us down. Why did I add in the message loop at all? Isn’t that just extra baggage if I don’t create a window for my thread? No, I added that so that we can send our own messages to the thread. We must tell the thread to stop executing and finish up before the main program (WinMain) quits. That’s why I defined a custom message – you can send it to the thread with PostThreadMessage(). Note that you also have to wait for the thread to shut itself down – so you need some way of knowing when it has shut down. Just create a global variable, called:
Just before the thread exits from its message loop, have it increment that variable and return 0;. When the variable reaches the number of threads you have running, all threads have stopped and you can safely exit from WinMain. Never, ever, use the TerminateThread function! If you noticed by now, I never told you how to actually spawn another thread. You do it with the CreateThread function:
Whew! Luckily, most of those parameters have defaults that are fine for almost any program or game. lpThreadAttributes should be NULL, as should dwStackSize, and dwCreationFlags. (Windows will assign the defaults, which are usually great – like Windows auto-configures the stack size ;-) ). lpStartAddress is the address of the ThreadProcedure, lpParameter is any pointer you’d like pass to your ThreadProcedure, and lpThreadId is the address of a DWORD that Windows will set to the ID of the thread (think DX functions, here). Here’s an example of how to use it, with our previous ThreadProcedure function:
How do I design a thread function? Threads are best for simple tasks that are repeated often (and then the thread Sleep(0)s ;-) ), or for complex tasks that run in the background but aren’t time critical (that is, you don’t care when they get run, as long as they get enough time. Think of it this way: if a thread just does a simple function call (like lpDIKeyboard->GetData()) and then Sleep(0)s, it’s a simple thread. But, if the thread does a lot of tasks – like printing a document each time before it Sleep(0)s, then you know it’s a complex thread. What’s the big difference? Complex threads tend to run sporatically, spending most of their time yielding to other threads, while simple threads run very consistently. The difference lies in the ratio of the time they spend executing their task to the ratio of their time-slice they give away with the Sleep(0) statement. Sporatic threads do best with lower priority, while simple threads do best with higher priority. You can change the priority of the thread with SetThreadPriority(), which also returns the previous priority for that thread. Note that you have to be very knowledgeable about threads and the scheduler to adjust thread priorities without a performance decrease. Also, ALWAYS, ALWAYS, ALWAYS put a Sleep(0) statement in each ThreadProcedure that you make! Otherwise, lower priority threads will never get a chance to execute. This can actually stall or even crash Windows if used incorrectly, but used correctly, it can increase the efficiency of your game. This is much too big of a topic to cover here, so I would suggest you get a book or find some good tutorials on multi-threading. :-) If you are feeling particularly brave with threads, I want you to try to move the game loop in my Switch Mode Demo – Part 2 program into a separate thread of its own. That’s right, the whole game loop! Just for kicks, and to get a feel for how multi-threading operates. Once you have it working, play around with the priority a little, or change the message loop. :-) Windows is just like communism – if everyone’s sharing, there’s plenty of resources to go around. Unfortunately, one poorly written program will ruin the whole thing (which is why, IMHO, communism never works ;-) ). However, the sharing motif is the only way to go with software in a multi-process multi-threading environment like Windows (or MAC or Linux or ...insert your favorite OS here). Just remember to release resources when you are done, and Windows will treat you kindly, too. Good luck with your Windows game programming! Contact me with any questions, comments, and corrections at: Ratt96963@aol.comMy web site is: http://www.freeyellow.com/members8/nullpointerI enjoyed writing this article, and I hope it encourages you to explore the world of Windows game programming for yourself. |