|
Better Threads For The VCL |
|||||||||||||||||||||||||||||||
This originally appeared as a two part series that started in the June 1998
issue of Visual Developer magazine.
Unless you've been living in a cave for the last several years, you know that one of the big features of Win32 systems is preemptive multitasking and the ability for each process to spawn multiple threads. A thread shares the address space of its parent process, so it can read and write its parent's global variables, but each thread has its own stack and register image, so that a process's threads can execute totally different code, more-or-less at the same time. Each thread can be assigned a different priority, so that a process can assign a lower priority to a long-running computation than to a short handshake over a socket. What's more, the thread API seems to have been designed with object oriented class libraries in mind: thread creation requires a 32-bit pointer to a function that takes a single 32-bit pointer argument. Unfortunately, threads are one of the very few places where Borland's VCL fails to provide a simpler, cleaner interface than the Microsoft API does. It's true that we don't have to supply security attributes and stack sizes to create a thread, but instead of calling a function with a simple "procedure of object" or "closure" parameter, we have to create an object which descends from TThread and which has a specialized Execute method. If you're like me, the first few time you used a TThread you created a new thread type for each thread, and put the actual thread code in the Execute method. What's wrong with that? It tends to violate encapsulation. Typically we want our threads to act with and on the program's existing objects: Background printing, perhaps, or running a lengthy calculation without peppering it with ProcessMessage calls. Putting the thread code in a thread object's Execute method means that the thread object has to know an awful lot about other objects. Often, too, the other objects have to create special fields just for sharing data with the thread, so thread creation can involve a lot of code. This is tedious. It's also a bad, dangerous coding practice for all the reasons that encapsulation is good, safe coding practice: making thread objects heavily dependent on other objects' internals creates opportunities for any small change in 'this' object to break the thread running on 'that' object, and creates the possibility of unrelated code breaking the 'contract'. Simple ThreadsA far safer and simpler approach is to simply have the thread run what Delphi calls a procedure of object and C++Builder calls a closure. (I'll call it a closure from now on, simply because it would be just too tedious for you and me to plough through "procedure of object or closure" each time.) Because a closure contains both an object's instance pointer and a pointer to one of its methods, there's no need to break encapsulation or to write a potentially large set of communication methods: The object operates on itself. Additionally, the thread stays completely generic: It doesn't need to know anything about the other objects in the system, and there's no need to create a new thread type for each thread. I've implemented this approach in the "Simple threads" section of Listings 1 (SynchedThreads.pas), 2a (SynchedThreads.h), and 2b (SynchedThreads.cpp). The first parameter to the RunInThread function is the closure to run in the new thread while the optional (in C++) second parameter is a untyped pointer that gets passed to the thread's closure. RunInThread creates a new TSimpleThread and passes its two parameters to TSimpleThread's constructor. The constructor in turn saves its two parameters in a pair of protected fields, then starts the new thread running. The new thread's Execute method then simply calls the closure. The "Beep" button of the test applets (Listings 3 ("PascalTestForm.pas"), 4a ("CppTestForm.h"), and 4b ("CppTestForm.cpp") and Figures 1 and 2) uses RunInThread to create a simple thread and return. You should be aware that your application's process will not terminate while any background threads are still running. This poses no special issues when the TSimpleThread you create directly or via RunInThread always terminates relatively quickly - the very worst thing that can happen is that your application will show in the Task List for a little while after it is no longer on the task bar - but there will be times when you need to abort a background thread. The AbortThread method does this for you, by suspending the thread and then freeing the thread object. A TSimpleThread's knowledge of other objects is limited to 2 (or, really, 3) pointers, passed in as parameters, and the same simple code can be used for many different cases. Both the new thread and the thread that created it run, more-or-less simultaneously, until the thread function returns and the thread shuts itself down. While this is just what we want in cases like Web service or background printing, there are other situations where we want to create a thread and then wait while it executes. Message WaitsFor example, despite all the speed of modern machines, as soon as we have to repeat a loop a few tens of thousands of times, or do a series of disk operations, we're facing distinctly macroscopic runtimes. If we don't pepper the process with ProcessMessage calls, our app suddenly fails to respond to keystrokes and mouse clicks. On the other hand, while those ProcessMessage calls slow down our already slow operation, the UI becomes sluggish if we don't make them often enough. The lengthy operation will be simpler and faster if we put it into a background thread, and leave the foreground thread doing nothing but responding to user actions until the background thread is done. For another example, when we send a request to a server over a network socket, we typically load a packet into a buffer and initiate a transfer, which then takes some relatively large amount of time to complete. We then have to wait another relatively long time for the server's response, which may come in several pieces. Our foreground code can be partitioned into a state machine that reflects this complexity, but our application will be considerably smaller and simpler if we can hide all the protocol and its waits behind a single, traditional-looking function that 'just' takes a long time to return. This is the sort of situation that the TThread WaitFor method would seem to have been created for: Spawn a thread, and go to sleep until it's done. Unfortunately, for some strange reason, WaitFor totally ignores all window messages. Calling WaitFor from an application's foreground thread leaves it as blind to user input (and paint requests) as if it had entered a long loop without ProcessMessage calls. Fortunately, the Win32 wait API includes a function called MsgWaitForMultipleObjects. This takes an array of Windows "object" handles, and does not return until there are messages in the thread's input queue or until one or more of the objects "signals" Windows. Microsoft promises that the wait is efficient, and that waiting threads have a very modest effect on system responsiveness. The Win32 help page for MsgWaitForMultipleObjects shows that you can pass it a wide variety of different objects, each of which signals in different ways. For example, threads and processes signal by terminating, while events signal when they are explicitly set. While in this article I use only threads, events, and processes, you should be able to use the full panoply of Windows wait objects with the MsgWaitForSingleObject function in the "Wait threads" section of Listings 1 (Pascal) and 2 (C++). MsgWaitForSingleObject is really very simple. It (repeatedly) passes a single object handle to MsgWaitForMultipleObjects and asks to get control back whenever the object signals or we have any messages to process. When the API wait function returns, we examine the return code. If that indicates that there are messages pending, we call the Application object's .ProcessMessages method. Otherwise, the API return code indicates that the object has signaled, so we break out of the infinite loop and return. Combining TSimpleThread with MsgWaitForSingleObject gives us the TWaitThread class. A wait thread is basically just a simple thread with a more sophisticated abort mechanism and a new MsgWaitFor method, which does a MsgWaitForSingleObject on the thread Handle. This wait ends when the thread closure returns, and the thread terminates. The "Wait threads" section also supplies the WaitForThread and MsgWaitForThread routines, which create a thread for you, just like the RunInThread routine, but which do not return until the thread function returns. As you might imagine, WaitForThread calls the (blocking) WaitFor method, while MsgWaitForThread calls the (nonblocking) MsgWaitFor method. Because you may need to abort a thread, the first parameter to MsgWaitForThread is a reference to a pointer to a TWaitThread. (In Pascal, this is a "var Thread: TWaitThread", while in C++ this is a "TWaitThread*& Thread".) While the waiting thread is in MsgWaitForThread, this pointer will be set to the thread that is running; when MsgWaitForThread returns, the pointer will be cleared (ie, set to Nil or NULL.) Thus, a method like the test applets' WM_CLOSE message handlers can tell if there is a thread wait that needs to be aborted by examining a "WaitThread" variable: if Assigned(WaitThread) then WaitThread.AbortThread; if (WaitThread != NULL) WaitThread->AbortThread(); As you can see, aborting a wait thread looks much the same as aborting a simple thread. However, when you wait for a thread, it's usually because you want to do something with the result of the thread's activities. Thus, aborting a thread that you're waiting for requires different 'semantics' than aborting a simple thread. If you simply kill the thread, the wait call will return even though any work the thread was doing was stopped at a random point. You generally don't want to try to do anything with the thread's partial results! We might want to make the wait call return some sort of "normal completion" flag but this seems to be a case where raising an exception is much better. We don't clutter the "normal" logic with lots of tests, and might be able to contain several different threaded operations within a single try block. Thus, whereas simple threads normally start running as soon as you create them, wait threads are created suspended. The thread will not run until you call either WaitFor or MsgWaitFor. Both of these routines will raise an EAbort exception if you call AbortThread. (Obviously, a thread that's sitting in WaitFor won't be able to call AbortThread, but a third thread could.) Not handling the EAbort exception will quietly shut down your application; you need to catch the exception if you want to take less drastic action. Wait threads allow us to run a potentially lengthy operation at full speed without explicitly adding any ProcessMessage calls. I'd like to say that the user won't notice any sluggishness in responding to input, but this depends on what the thread is actually doing. A thread that spends most of the time waiting for a peripheral like a disk drive, modem, or network card is "IO-bound", and will generally not have much effect on the foreground tasks' responsiveness. On the other hand, threads that do a lot of calculations are "compute-bound" and will use all of every "quantum" the system gives them. Windows won't be able to check the message queue as often as it normally does, and your application will show the same sort of sluggish response to user input that it does while a large application like Netscape loads. My experience has been, though, that you usually get foreground performance from a wait thread that's quite comparable to what you get from code full of ProcessMessage calls, and the overall wait is usually substantially less. In addition to the performance advantages, using a TWaitThread can make an application much clearer. For example, I wrote a custom news reading application that used the NetManage NNTP OCX that comes with Delphi 3 and C++Builder 3 to scan various newsgroups for contract opportunities. The design of the OCX is rather crude and most operations result in one or more calls to various event handlers. This forced me to write my application as a state machine: before every call to the OCX, I set pointers to a OnInput handler and an OnCompletion handler. The DocOutput event then used these pointers to call the appropriate pieces of code. This did work, but meant that every little piece of interaction had to be in a separate method, and that following the flow of control required a lot of jumping around the source. It was not easy to read or to change the code. By contrast, when I replaced the NetManage OCX with a component I wrote that used blocking sockets and TWaitThread's to make the various NNTP requests look like traditional function calls, I was able to write a single top-level procedure that made a series of perfectly self explanatory calls, each of which in turn decomposed their functionality in the good, old-fashioned, top-down way. Much easier to write, to read, and to modify! Stop/Start ThreadsTWaitThread's do have one major drawback. When the object method that you pass them returns, so does their Execute procedure. They then FreeOnTerminate, which entails calls to the VCL memory manager and to the API ExitThread function. This sort of overhead is negligible for operations like database queries that are initiated at rare intervals, in response to user input, but it can add up when we want to repeatedly pass an operation to a thread and wait for it to complete. For example, a simulation may run its calculations in a thread and update the display after every pass. Similarly, sometimes my news reading application finds many thousand messages that pass its subject line filters. It has to then get the body of each one and apply filters to it. Creating and destroying a thread object for every 'get body' request imposes enough overhead that the process is no longer modem bound! The TStopStartThread in the "Stop/start threads" section of Listings 1 and 2 solve this problem by replacing the wait thread's simple 'call one function and return' approach with a loop that calls Suspend after the closure returns. The thread then sits idle until some other thread calls Run again. Instead of waiting on the thread handle, and thus returning from the wait when the thread terminates, stop/start threads wait on a Windows "event" object. This event is cleared every time you call Run, so the calling thread waits until the closure returns and the stop/start thread's Execute procedure sets the event. Using a TStopStartThread is a bit more complex than using a TWaitThread, because you have to explicitly create and free the TStopStartThread object yourself - but it takes about one hundred times as long to create and run a wait as to run a method in an existing stop/start thread. Aborting a stop/start thread is much like aborting a wait thread, in that calling AbortThread will raise an EAbort exception. While this exception is a direct response to a legal - though presumably unusual - action by your program, the TStopStartThread can also raise a couple of error exceptions when you use it improperly. Calling Run while the thread is already running will cause an EThreadInUse exception. (Note that you can create multiple stop/start threads - it's just that each can only do one thing at a time.) Aborting a stop/start thread leaves it suspended in the middle of its closure, so it can't be restarted. Thus, calling Run after an abort causes an EAbortedThread exception. SynchronizationSome of you may have been wondering "What about synchronization? The VCL is not completely thread-safe, and we can't do things that have visible effects within threads. With the TThread relegated to a piece of low-level code that knows nothing about the code it's running, how can we use the Synchronize method?" One solution (at least with stop/start threads) is to use the optional untyped pointer parameter to pass a pointer to the thread object into the closure that the thread runs. This pointer can then be used to call Synchronize. But really, why bother? Not only does calling Synchronize suspend the thread, using Synchronize has, for me, always been fraught with the same sort of communication-via-state-variables encapsulation violation problems as putting thread code into the Execute function. In many cases, we simply don't need to use Synchronize. Most of the code that I've put into threads has naturally fit into a TStopStart thread: I wait for a computation, or a response, and then do any display in the foreground thread. For example, a simulation updates a TBitmap in a thread and then wakes the foreground thread and suspends itself. The foreground process draws the bitmap on the screen, updates a few labels, then wakes the background thread and goes into another MsgWaitForSingleObject. The foreground code consists of a loop that runs a function in a thread then displays the results. Sometimes, though, this sort of wait-update loop isn't adequate: we need to continuously display the status of a long-running background operation. In these cases, we can safely update the display by using the PostMessage API call to asynchronously send messages to a form's window handle. PostMessage simply places a message in the window's message queue and returns, so the background process is in no danger of calling any potentially non-reentrant VCL code. Messages are always processed in the thread that created the window handle - the foreground thread - and message handling by the foreground process is thread safe as the VCL doesn't call ProcessMessages in the middle of any non-reentrant functions. Not only does using PostMessage pose fewer communication problems than Synchronize as you can pass a wParam and an lParam, calling PostMessage does not suspend the thread the way Synchronize does. Other Uses For WaitsSo far, we have only used MsgWaitForSingleObject for synchronizing the activity of a couple of threads. While it can be used in many more ways, some of them are relatively esoteric. I'll just quickly mention a couple of other uses that fit in with this article's general theme of using waits to put a simple, old-fashioned, procedural face on complex, asynchronous code. For example, you might want to start another program running, and then wait until it finishes. This can be useful if you are shipping a system consisting of several different executable files, or if you need to read the files a program writes, but don't want to look at them until it's done. The SpawnProcess function in the "Wait threads" section of Listings 1 and 2 uses the API CreateProcess call to run an arbitrary DOS command, and returns a record containing the handle and ID for the process and its primary thread. If you then pass the hProcess to MsgWaitForSingleObject, you have a line of code that acts just like the old DOS "exec" function: // Pascal procedure Exec(CommandLine: string); begin MsgWaitForSingleObject(SpawnProcess(CommandLine).hProcess); end; // C++ void Exec(char* CommandLine) { MsgWaitForSingleObject(SpawnProcess(CommandLine).hProcess); } For a rather different example, we often need to ask our users questions with only a few possible answers: Yes or No; This or That; Abort, Retry, or Ignore. We can use MessageBox to pop up a standard dialog box, or we can write our own little modal form, but many users find a lot of pop up boxes to be annoying and distracting. Especially if we need to ask the same questions over and over again, it may be better to place a few buttons in a panel on the form that is already up. This minimizes delay and flashing, and limits the amount the user has to move the cursor. While our users will probably prefer this approach, we've made our life much harder. The popup form was modal, and we could simply call a single function to create a popup and get the user's response. Now, we are asking the question in one place, and getting the answer in another - the buttons' event handlers. Not only is this sort of decoupled code harder to read than a single, straight-through procedure; it's also much riskier. Both 'sides' have to 'agree' on how to interpret various pieces of global state and minor changes on either side might violate this agreement - and, of course, there's always the danger that a 'third party' might mangle the object fields that the two sides are using to communicate. It would be much nicer if we could simply call a single function that didn't return until the user pressed a button. We can do this by creating, and waiting on, an event object. The button event handlers take some action that lets us know which button was pushed, and then set the event. At this point, our wait returns, and so does our 'modal function'. The test applets that accompany this article (Figures 1 and 2, Listings 3 and 4) illustrate this technique in its most rudimentary form: When you press the "Wait" button, the EventWaitBtnClick event handler resets the UntilFlag event, and then waits for it to be set. When you press the "Until" button, the UntilBtnClick event handler sets the UntilFlag event, which allows the EventWaitBtnClick routine to finish. In a more realistic use of this technique, the query function might set a pointer to its local Result variable in the form object. The button event handlers would then deference this pointer to set the Result - perhaps to their Tag field - in addition to setting the wake-up event. Summary
This article originally appeared in "Visual Developer" magazine. Copyright © 1998, Jon Shemitz - jon@midnightbeach.com. Jon Shemitz is an independent consultant and a frequent contributor to Visual Developer. He can be reached at http://www.midnightbeach.com.
|
||||||||||||||||||||||||||||||||
Created on 1998, last updated July 26, 2002
Contact jon@midnightbeach.com
|