More First Aid for the Thread Impaired: Cool Ways to Take Advantage of Multithreading
| This article assumes you're familiar with Win32 and MFC|
Code for this article: Multithreading0797.exe (20KB)
Russell Weisz is a software developer at The Windward Group, a Los Gatos, California-based company that provides software research and development, quality assurance, and documentation services. He can be reached at firstname.lastname@example.org.|
Multithreading is a powerful technique that can improve the performance, responsiveness, and structure of your application. At the same time, multithreading introduces synchronization, debugging, and other complexities into your code and development cycle. In a previous article entitled "First Aid for the Thread-Impaired: Using Multiple Threads with MFC" (MSJ, December 1996), I discussed some of the reasons why you would consider using multithreading and reviewed some
of the related MFC classes and Win32® mechanisms. In this article, I'd like to continue with this thread (sorry, no more bad jokes from now on). I'll clarify a few points from the last article, and discuss my seven favorite ways to use multithreading.
In my last article, I discussed three ways to create multithreaded objects. Two of them are provided directly by the MFC class CWinThread. You can pass CWinThread a pointer to a function that the new thread will execute. This is called a worker thread. Alternatively, you can derive a class from CWinThread and override a virtual method, such as OnIdle, to do your specific work. This is called a UI thread. I also presented a third approach, using a class called CMultiThread. Derived from CWinThread, CMultiThread uses CEvent objects to block when there is no work for the second, internal thread to do. When it is time to do some work, the CEvent object is used to notify the second thread to wake up and get busy.
Multithreaded Workflow Application
Recently, I had an opportunity to use several instances of CMultiThread in a rather demanding application. You may know my customer for this application, CTB/McGraw-Hill. They supply and score the placement and proficiency tests that a child may take in school. The application I developed for them is called Scoring Production Control Software (SPCS). It's a flow-control system for the huge amounts of student and test data that makes its way through their scoring process. And no, it's not my fault if you had to repeat third grade!
Instances of a class called CJobMgr, derived from CMultiThread, guide the processing of large data clusters called OpUnits. The data is transformed in a sequence of operations which occur on different high-end servers. The servers have multiple CPUs, and a separate thread runs on each CPU to maximize the throughput of data. The servers are permanently assigned to do one of three different
types of data processing. An OpUnit must flow across all three designated server types to complete its processing, as shown in Figure 1.
|Figure 1: Servers with Data Flow|
One valuable lesson that I learned in developing this application is how to better handle the removal of embedded thread objects. CJobMgr instances are terminated only when a server must be brought down, but some limitations in shutting down the secondary threads became apparent. The problem results from a subtle race condition. The original CMultiThread class terminates its secondary
thread when the destructor is called. (The destructor code is traversed on the caller's, or primary, thread.) Of course,
the base class destructor code is executed after the derived destructor, and that's the root of the problem. CJobMgr's secondary thread can still access the derived object data after the derived object has been destroyed, but before
the base class can kill the secondary thread. I added a
new protected method to CMultiThread, called KillThread2, which must be called in the derived class's destructor. Unfortunately, this goes against my desire to encapsulate threading knowledge completely within the base class. The Run method also had to be modified. If you look at Figure 2, which includes the code for KillThread2, Run, and the derived class destructor, you will notice the call to AfxEndThread at the end of Run. This call is essential for cleanly removing the secondary thread. I adopted it from a discussion in MFC Internals (Addison-Wesley, 1996) by George Shepherd and Scot Wingo.
Even with this fix, CMultiThread has one undesirable quality: it's big and bulky. That's because it is derived from CWinThread. CWinThread is a large, extremely complex class. It has a tremendous amount of functionality, most of which you will not use except to support message handling in UI threads. Since CMultiThread was not primarily designed to support UI threading, my thread class has acquired some unnecessary bulk.
Having said this, the solution to developing a leaner embedded thread class is now obvious: let's get rid of CWinThread. Figure 3 shows the definition and code for this new class, which I call CThinThread because it has lost the CWinThread baggage. CThinThread spins off its secondary thread via a call to the runtime library function, beginthreadex. I considered using beginthread or the Win32 function CreateThread instead of beginthreadex, but they both have important limitations. beginthreadex offers more flexibility than beginthread, allowing me to specify Windows NT® security attributes (under Windows NT). It also corrects some internal race conditions that exist in beginthread. Finally, beginthreadex does some thread local storage initialization so you can use the multithreaded C runtime library functions. CreateThread doesn't and is, therefore, incompatible with C runtime calls. For a more complete discussion of CreateThread, I recommend that you take a look at Jim Beveridge and Robert Wiener's Multithreading Applications in Win32 (Addison-Wesley, 1996).
When it's time to end the secondary thread at the bottom of the Run method, I close the thread handle and then
call endthreadex. I have made a few other simplifications
in adapting CThinThread from CMultiThread, but it's basically very similar. The StartWork method allows you to do some initialization on the second thread in derived classes. Similarly, EndWork can be overridden to add some completion code before the internal thread terminates. DoWork must be overridden to do any repetitive, separately threaded activity.
Note the strangely typed static method, Start. A pointer to Start is passed into beginthreadex, instead of a pointer to a C function. Once inside Start I cast the generic pointer, which is passed into the method back to a pointer to my CThinThread instance. Now that I'm running on the new, encapsulated thread, I call my Run method and get to work. CThinThread's biggest drawback is that you can't use it to create new windows. I'll discuss that in a little while.
Active versus Passive Multithreading
Now let's look at some applications for multithreaded objects. I want to make a distinction between what I'll call active and passive multithreading, which affects my choice of applications. In active multithreading, I am directly responsible for creating and removing all additional threads. This contrasts with passive multithreading, where some other entity can create threads that my application or component can use. An important example of passive multithreading is COM servers, such as ActiveX™ controls. You can use one of several approaches to couple the server with the client: the single or multithreaded apartment model, or eventually the free-threaded model. The approach you choose affects the design of the control in terms of issues like thread safety, but not direct thread creation and removal. See "Give ActiveX-based Web Pages a Boost with the Apartment Threading Model," by David Platt in the February 1997 issue of MSJ for an excellent discussion.
Another passive threaded framework is the ISAPI environment on Web servers. You could develop a thread-safe DLL for use in ISAPI. The DLL could then support concurrent threads of execution. Actual thread creation and removal would be done outside of your DLL and outside of your direct control, however. Passive multithreading is certainly used in interesting ways, but the multithreading is provided for you. Having made the distinction, this article will cover only active threaded architectures. Specifically, I'll cover my seven favorite ways to use active threaded architectures.
Number 1: Refreshing a List Control with Lots of Data
List controls and list boxes are designed to display rows of data, and sometimes applications must use them to present many rows or records. This isn't necessarily a difficult problem, and there are multiple ways to solve it. In CTB/McGraw-Hill's SPCS, most of the list control displays are tidy little affairs populated by several hundred or fewer SQL Server records from a single, heavily indexed database. The OpUnit Status view, shown in Figure 4, and the similar-looking Restore Archives view are different, however. Although the user can filter the record set, the worst-case scenario involves retrieving and displaying five or ten thousand records, maybe even more. It can take a minute to get all those records and load their contents into the list control, even on a Pentium 200 class machine.
|Figure 4: OpUnit Status View|
As I mentioned, there are a couple of ways to solve this problem, and some of you may already be thinking: make the list control owner draw and only get the record contents when they must be displayed. Certainly, but that doesn't fit easily into my existing architecture, so instead I made a separately threaded class to retrieve the records and refresh the list controls.
Figure 5 contains the definition and code for this class, CRefreshThread. It's multiply derived from CThinThread and CPageBase. CThinThread provides the internal threading functionality. CPageBase is a "mix-in" type base class originally developed for all the tabbed views, such as the OpUnit Status view shown in Figure 4. These views are multiply derived from MFC's CPropertyPage and our CPageBase. CPageBase has protected methods and members to retrieve records. CPageBase was designed to be a base class and most of its useful methods are protected. In the future, I may redesign CPageBase so that I can use it to compose other classes, rather than inherit from it.
CRefreshThread primes the record retrieval pump in StartWork with calls to the record manager to GetAllOrderedRecords and GetFirstRecord. The record flow continues through DoWork and then is tidied up in EndWork. CPageBase also has protected methods to load list controls. Alert readers who have already scanned the listing
are thinking: what list controls, and what are these stinking CListCtrlExs? CListCtrlEx is a list control (derived from CListCtrl) that inverts all fields in the row to indicate
row selection. The class is adapted from CListViewEx in
the RowList sample distributed with Visual C++® 4.2
Enterprise Edition. If you want to use that class, plan
on doing some work to clean up a few minor painting glitches, such as the one that occurs when the column widths are minimized.
Getting back to CRefreshThread, let's check out a few other points of interest. What's up with those window handle members? If you refer back to my previous article, you'll recall a discussion about problems in some MFC classes when they are shared between threads, due to handle mapping issues. The problem didn't actually show up bouncing balls around the screen in the previous article, but it does show up here. To share the list controls, I pass in the handles as parameters to the Set method. When the internal thread starts up, it attaches new list control objects to the handles, which accomplishes the binding that I need to use the CListCtrlExs. Just before the thread terminates, it detaches the objects from the handles to avoid destroying the actual list controls. This process allows me to share MFC list control objects, but it doesn't make them thread-safe. The tabbed views that have CRefreshThread members are careful to allow only one refresh operation to occur at a time. CThinThread's IsBusy method provides the information used to disable appropriate menu and toolbar options when a refresh is in progress.
Now, if you pay attention to experts like Mr. Beveridge and Mr. Wiener, you will know better than to even try to access MFC objects in threads initiated with beginthreadex (or beginthread). So why am I doing it here? Well, because I can. The attachment of MFC objects to handles and the subsequent use of those objects works even with beginthreadex. I could have used CMultiThread here, but I opted for the lightweight approach instead. A generic CWinThread-derived class (a UI thread) would be considerably more difficult to control or remove. A worker thread is a bad choice here, as always, because it puts you back in the C universe, which is not a happy place to be in an otherwise object-oriented architecture. So, deriving from CThinThread is workable for the kind of activity required of CRefreshThread, but if you want to create MFC windowing objects strap in tight, because there are some new bumps in the road.
Number 2: Launching Other Processes and Waiting for Results
Sometimes it's necessary for one application to start another. There are two situations that require CTB/McGraw-Hill's SPCS application to launch other processes. CJobMgr, mentioned previously, controls the work flow. It reads a sequence of operations from a file, does some minimal parsing of each operation's input and output description, and then performs the operation. The operation is either a CLocalOp, an operation that is handled locally inside SPCS, or a separate application that can be started via the Win32 function CreateProcess. Figure 6 shows CJobMgrImp, the implementation class derived from the CJobMgr abstract base class (ABC), method DoTheOp. There are a few points of interest to note in this method. A file (disk or memory-based) is used to transfer input and output parameters between the CJobMgr and the local or remote operation. The SetParameters and GetParameters methods are responsible for loading and unloading values from the shared file. The file name, contained in the variable m_csIOFile, is passed to the operation. If I am starting a process, I must allocate STARTUPINFO and PROCESS_INFORMATION structures and pass them to CreateProcess. There is nothing for the CJobMgr instance to do until the process completes, so I block using WaitForSingleObject. After unblocking, I must remember to release the handles created in starting the other process.
Now that I've covered the details of this implementation, I'll back up and review the architecture. On each server, I create a small set of CJobMgr instances, and each instance has its own internal thread. The CJobMgr assigns itself to an OpUnit, goes through a sequence of operations, releases the OpUnit, and starts over again. This approach is simple because at any point in time there are n CJobMgr instances, n or fewer OpUnits being processed, and the sequence of operations are always very similar.
Another way to approach this problem is to create a single scheduler instance on each server and treat it like an operating system. In this scenario, each OpUnit has an associated control block that contains timing and state information. The scheduler wakes at appropriate intervals, checks and updates the chain of control blocks in order to decide what activities must occur, and returns to sleep after the pending activities are completed. This can be handled efficiently using WaitForMultipleObjects with an array of launched processes for which I am awaiting completion. The two drawbacks to this approach are increased complexity and poor utilization of multiple CPUs because the local operations must be executed sequentially.
Now I have to decide how to implement my multiple threads. I chose to derive CJobMgr from CMultiThread. CJobMgr is a big class, so I was not too concerned about CMultiThread's size. CThinThread lacks CWinThread support. CJobMgr instances do need to create window objects, though, and CMultiThread's derivation from CWinThread allows it with ease. Direct use of CWinThread in either the worker or UI thread flavors is disadvantageous for the usual reasonsC implementation, poor encapsulation, poor control, and so on.
Number 3: Launching Other Processes, Take Two
SPCS also provides an interface for the user to launch separate applications. It is found in three of the tabbed views: Online Updates (shown in Figure 7), Browser, and Online Reader. The user selects a row, which represents an OpUnit, and launches the other application from a menu item or toolbar button. The three tabbed views differ only in the OpUnits presented and the application that each can launch.
Figure 7 Online Updates View
This time let's talk about architecture before implementation. Why do I need to start another application? Why not just access some code in a DLL? There are several reasons. SPCS is a control system built around a set of existing applications. The other applications are written by another development group in Visual Basic®. This kind of application partitioning is typical of what you find both for in-house software tool sets and in software product suites.|
In regard to starting the other applications, the requirements are different here than in the CJobMgr. The user must be able to start as many copies of the applications as they want, without delay, from these tabbed views. When the other applications complete and the return status is updated in the appropriate database record and shown to the user, the activity is done. Here, launching is driven by user input and terminates when the activity is over. A CJobManager instance, on the other hand, is active throughout a server session, continuously launching one application in a sequence. CThinThread is the preferred base
class for my tabbed views. Its lighter weight better suits having an indeterminate number of instances which are relatively short-lived. Two problems loom into view, however, in considering the use of CThinThread. First, I'll need to display a message box if the launched app returns an error. Second, I'd like to destroy my independently threaded launch object at the time its internal thread has completed its work.
These two issues can be resolved, and Figure 8 shows
my solution. CLaunchThread, derived from CThinThread and CPageBase, is very similar to CRefreshThread. The Set method takes an unusual collection of parameters: a handle to the parent window, a handle to a CListCtrlEx, an HCURSOR, the name of the application to launch, and an OpUnit. The application name is obvious because I need to know what to launch (see CPageBase::LaunchApp). The OpUnit name is used several times, and the CListCtrlEx allows me to update the display in DoWork. I need the parent window to post it a message when my secondary thread terminates. CUpdatesPage::OnCommand receives this message and calls its base class method, CPageBase::
LaunchDone. LaunchDone provides the answer to my problems by displaying a message box, if required, and then deleting the CLaunchThread object. I can't delete the object directly from its internal thread because the context is wrong, so this indirect approach is the next best thing. Finally, I change the cursor prior to creating CLaunchThread to provide user feedback, and then I restore it inside CLaunchThread after a few seconds.
CUpdatesPage is the class associated with the Online Updates tabbed view (shown in Figure 7). Its OnBegin method is called when the user selects the Launch Application menu item or toolbar button. This is where the new CLaunchThread object is created and activated using its Set and Go methods. As you look at the code, you may find it confusing that both CUpdatesPage and CLaunchThread call CPageBase methods like LaunchApp and LaunchDone. Remember, though, that both are derived from CPageBase.
Most of LaunchApp should look familiar. It closely resembles CJobMgrImp::DoTheOp (from Figure 6). Both use CreateProcess to start the appropriate application, but LaunchApp must do a little extra work after the launched application completes. It calls a static method from the shared parameter file class, CParamFile::Code2String, to convert a returned value. I needed to introduce a slight delay before removing a disk-based parameter file. Presumably, there is some residual low-level activity that needs to complete after the launched app writes to the file, closes it, and exits. I included one additional method, CSpcsView::CheckThreads, which is called by CMainFrame when the application tries to exit or the session tries to end. To avoid memory leaks, I prevent either from proceeding while any launch or refresh secondary threads are running.
Number 4: Archiving and Restoring
In SPCS, all the data in an OpUnit is stored in a database. These databases can be sizable, ranging from megabytes to gigabytes. After an OpUnit has passed every processing operation, it can be moved to a backup medium, such as tape. In fact, it must be or the server disks eventually will overflow. In certain situations, this archived OpUnit must later be restored back to disk. This kind of functionality is common in applications that produce significant amounts of data. The medical application in my last article had a similar requirement to periodically save data, which is acquired and processed on the client, out to the server and to floppy disks. Moving large data sets from one storage medium to another is a natural activity for multithreading.
I'm just starting the design for this component, which I call the ArchiveMgr, so I can give you some preliminary thoughts about how I plan to implement it. Unfortunately, there is no code available to see. Archiving and restoring an OpUnit both consist of several sub-operations that must be performed serially. The database is dumped to a file, the tape may need to be positioned, identifying information may need to be written to the tape, the file is transferred to the tape, and results are checked and updated in a status record. This already sounds reminiscent of the set of operations choreographed by CJobMgr, which I examined in method Number 2.
Let's consider some other issues that affect ArchiveMgr's component architecture. I anticipate that ArchiveMgr will sometimes have to display message boxes or other windows. Also, ArchiveMgr's internal thread can persist as long as the SPCS session remains active on the archive / restore server. Only one tape device is planned for this system, so only one tape operation will be active at a time. This suggests that I create only a single instance of ArchiveMgr. I could potentially overlap activities that depend on separate media and make dumping or reconstituting a database concurrent with writing or reading a file from tape. If overlap is not required, ArchiveMgr could be based on CMultiThread and resemble CJobMgr. If I want to support overlap, then ArchiveMgr would need to keep track of the current state for each overlapping operation sequence. Alternatively, each concurrent activity could be handled by a separately threaded instance of a subcomponent of ArchiveMgr. I could have three subcomponents: CDumpDatabase, CRestoreDatabase, and CTapeInterface, each based on CMultiThread.
While I'm on the subject of accessing slower devices like tape and disk drives, let me mention another cool thing I recently discovered while perusing Mr. Beveridge and Mr. Wiener's handy bookthe I/O Completion Port. This is available under Windows NT since version 3.51, and it may prove useful in managing your multiple threads. If you expect to initiate several files named pipe, or socket access operations in parallel using overlapped I/O operations, then you can create an I/O Completion Port and use it to bind your available threads to I/O request completion. When the file read or similar operation completes, the port you created will activate a waiting thread from a thread pool and pass it the results of the operation. All you have to do is create the threads, create the file handles, set up the completion port, let the threads wait on GetQueuedCompletionStatus, and the rest happens automatically. Because the I/O Completion Port is provided by the operating system, I presume that it is a very efficient mechanism. Consult the Beveridge and Wiener book for an in-depth discussion and examples.
Number 5: Resolving Errors Asynchronously
Errors happen, and either you handle them or they handle you. Error notification, be it an assertion, an exception, or a return value indicating failure, is typically synchronous and in the thread which caused the error. This is a good thing, but in a multithreaded application you may want to postpone error resolution and focus instead on completing the work that is going successfully. Additional considerations may come in to play. Error resolution may be poorly suited for the thread that caused the error because special operations, or special priorities, are required. You may even prefer to collect several errors, and possibly reorder them, before attempting any resolution.
In SPCS, I have separated error resolution from normal operation. CJobMgr is well equipped to process OpUnits as long as all goes well. But when an error occurs in one of the steps initiated by CJobMgr, it's time to enlist the services of a specialist like ErrorMgr. ErrorMgr runs on a different thread than any CJobMgr instance and is responsible for trying any automated activities that might resolve the error. If ErrorMgr can't handle the problem, then user intervention is required.
Today, SPCS is still in its youth, not yet having achieved the splendor of maturityright, but the point is that ErrorMgr is still pretty simple, with a limited repertoire of automated error responses. Today's ErrorMgr determines if a step can be retried, and if it can, changes the OpUnit state which allows a CJobMgr instance to retry it. In keeping with this simplicity, ErrorMgr is currently activated by timers, rather than having its own free-running thread. Back in method Number 2, I contrasted using a job scheduler versus multiple threads to initiate asynchronous activities. ErrorMgr follows the scheduler approach. When a timer event fires, it checks to see if any OpUnits need assistance, attends to them, and goes back to sleep when the current chores are done.
Timers are useful constructs. Extremely easy to use, they provide asynchronous activation on the thread that made the SetTimer call. They are not preemptive, or necessarily even punctual. In a Windows NT 4.0 environment you can call CreateWaitableTimer to get access to a more powerful timer which can be used with WaitForMultipleObjects and related APIs.
Still, timers do not scale well or disperse across multiple CPUs. If automated error resolution in SPCS gets complicated, I may change ErrorMgr's internal architecture. What would be an example of complex automated error resolution? Suppose I can get any one of several specific error codes as a result of attempting an operation like editing a database record. First I must map this code to a response strategy; certain strategies might require that a sequence of steps be executed, and some steps could take a while to complete. If I can't edit the record, perhaps I'll try to lock the table and then edit it. Suppose also that different information must be monitored during these steps than during normal operation. Perhaps I need to get certain relevant database parameters.
Given these characteristics, I would change ErrorMgr. Rather than using timers, I would use internal threading. In fact, I expect this ComplexErrorMgr would then closely resemble CJobMgr. I could have a fixed number of ComplexErrorMgr instances to handle the error flow, or I might prefer to dynamically create and destroy instances so that errors can be resolved immediately. Again, I might derive from an intermediate class that closely resembles CJobMgr, but is based on CThinThread (to stay lean).
Number 6: Handling Request Packets
Servers typically get bombarded by requests to do things. If a server is highly specialized, all of the requests may be for one type of service. If not, the server will be asked to perform various kinds of activities. These requests can arrive at the server by different meansphone lines, network cables, and so on. Captured by a driver, bubbled up the protocol stack, they arrive at a socket named pipe, or a similar communications or IPC terminus. I might have a dispatch module that monitors the socket (or equivalent), parses the input stream to form request packets, and passes each packet on to a handler. Alternatively, I could use the I/O Completion Port, briefly mentioned earlier, instead of a dispatcher.
Now let's focus attention on the handler component. Because servers often have multiple CPUs and requests tend to be asynchronous and high priority, it often makes sense to handle each request on its own thread. I can create handler threads dynamically as requests are received, or I can maintain a pool of handler threads. The thread can terminate when the request is completed, or it can be deployed again when another request has arrived and is ready to be handled.
Satisfying a request often involves retrieving data from a file, database, or memory, perhaps doing some operations on the data, possibly updating some state or associated data, and returning information to the requester. Figure 9 provides an illustration of a handler (CRequestHandlerThread) and a packet (CRequestPacket). CRequestPacket provides a framework for doing specific operations to handle a request. It does this by defining pure virtual functions for these operations which must be overridden by a concrete derived class. Once again, I could use an I/O Completion Port in my concrete RetrieveData method, if appropriate.
Packets are added to CRequestHandlerThread's work list in the AddRequest method, and extracted in DoWork. I used a CObList as the packet container, however numerous other MFC or STL containers could easily substitute. The ObList is made thread-safe with a CCriticalSection. Of course, there is no need for a packet container in an object derived from CRequestHandlerThread that has a lifetime of only one request. When the object is finished with a request packet, it posts a message containing a pointer
to the packet back to the dispatcher. The dispatcher can then route the packet back to its point of origin. If I did
use an I/O Completion Port instead of an inbound request dispatch module, I would still need an outbound dispatcher to catch my message and return the request.
CRequestPacket clearly is not a full-fledged packet class. It only hints at the information you would want to pass in a real application. Also, data members should be accessed using accessors, rather than directly. Again, this example is only intended to serve as a model for the classes that would be required to implement a real, high-throughput server handler. With some additional work, though, I could contemplate using this kind of approach in a Web server, a file server, or the second-tier server in a three-tier system.
Number 7: Parallel File Searching
My final favorite use for multithreading is hypothetical and again has nothing to do with SPCS. Say I wanted to search for copies of a file on multiple storage devices and I needed the results as quickly as possible. I could develop a file search class with an internal thread, then create several file search instances, and tell each instance to search a different device. Each instance posts a message to the parent window if it finds a copy of the file.
Figure 10 shows a CThinThread-based class, CFileSearchThread, which does a simple search. The DoWork method calls FindFirstFile to look for a specific file in a specific directory on the internal thread. If DoWork posts a file found message, the parent window can display appropriate user feedback.
In order to be more useful, my class could do a recursive search from an input root directory. This requires additional navigation logic and a change to my message posting. Either the message would have to pass a pointer to a string containing a found directory, or the CFileSearchThread instances would instead directly update a thread-safe display window or container class. (I leave these changes as an exercise for the ambitious.) Even on a single CPU machine, CFileSearchThread (especially the recursive variety) will provide more timely results than serially searching multiple disks, CDs, tapes, and so on. This is due to the fact
that seek and read operations on independent storage devices can be run concurrently. (Refer to Figure 1 in my previous article for a simple diagram that illustrates this concept.) The user interface for this recursive searcher could be simple text indicating each device and path
where the file was found. Or it could be a more graphical representation, perhaps using a tree control and bitmaps to more closely resemble the Windows® Explorer and Windows NT Explorer.
In this article I have tried to show several situations that either require multithreading or are significantly improved by multithreaded solutions. My choice of examples are, of course, biased by my experience. But I believe that the examples I discussed contain enough complexity and real world requirements that they may be helpful for other developers encountering different requirements. I specifically steered away from passive applications of multithreading, touched only lightly on uses of inter-thread and inter-process communication in multithreading, and undoubtedly missed other important realms to consider. I suggest you look at the growing body of multithreading literature for more coverage. Finally, I would like to thank the folks at CTB/McGraw-Hill for presenting me with interesting problems to solve and their generosity in allowing me to present this discussion and code.
© 1997 Microsoft Corporation. All rights reserved. Legal Notices.