Programming Windows with MFC, Second Edition

[Previous] [Next]

As far as Windows is concerned, all threads are alike. MFC, however, distinguishes between two types of threads: user interface (UI) threads and worker threads. The difference between the two is that UI threads have message loops and worker threads don't. UI threads can create windows and process messages sent to those windows. Worker threads perform background tasks that receive no direct input from the user and therefore don't need windows and message loops.

The system itself provides two very good examples of how UI threads and worker threads can be used. When you open a folder in the operating system shell, the shell launches a UI thread that creates a window showing the folder's contents. If you drag-copy a group of files to the newly opened folder, that folder's thread performs the file transfers. (Sometimes the UI thread creates yet another thread—this time a worker thread—to copy the files.) The benefit of this multithreaded architecture is that, once the copy has begun, you can switch to windows opened onto other folders and continue working while the files are being copied in the background. Launching a UI thread that creates a window is conceptually similar to launching an application within an application. The most common use for UI threads is to create multiple windows serviced by separate threads of execution.

Worker threads are ideal for performing isolated tasks that can be broken off from the rest of the application and performed in the background. A classic example of a worker thread is the thread that an animation control uses to play AVI clips. That thread does little more than draw a frame, put itself to sleep for a fraction of a second, and wake up and repeat the process. It adds little to the processor's workload because it spends most of its life suspended between frames, and yet it also provides a valuable service. This is a great example of multithreaded design because the background thread is given a specific task to do and then allowed to perform that task over and over until the primary thread signals that it's time to end.

Creating a Worker Thread

The best way to launch a thread in an MFC application is to call AfxBeginThread. MFC defines two different versions of AfxBeginThread: one that starts a UI thread and another that starts a worker thread. The source code for both is found in Thrdcore.cpp. Don't use the Win32 ::CreateThread function to create a thread in an MFC program unless the thread doesn't use MFC. AfxBeginThread isn't merely a wrapper around the Win32 ::CreateThread function; in addition to launching a thread, it initializes internal state information used by the framework, performs sanity checks at various points during the thread creation process, and takes steps to ensure that functions in the C run-time library are accessed in a thread-safe manner.

AfxBeginThread makes it simple—almost trivial, in fact—to create a worker thread. When called, AfxBeginThread creates a new CWinThread object, launches a thread and attaches it to the CWinThread object, and returns a CWinThread pointer. The statement

CWinThread* pThread = AfxBeginThread (ThreadFunc, &threadInfo);

starts a worker thread and passes it the address of an application-defined data structure (&threadInfo) that contains input to the thread. ThreadFunc is the thread function—the function that gets executed when the thread itself begins to execute. A very simple thread function that spins in a loop eating CPU cycles and then terminates looks like this:

UINT ThreadFunc (LPVOID pParam) { UINT nIterations = (UINT) pParam; for (UINT i=0; i<nIterations; i++); return 0; }

In this example, the value passed in pParam isn't a pointer at all, but an ordinary UINT. Thread functions are described in more detail in the next section.

The worker thread form of AfxBeginThread accepts as many as four additional parameters that specify the thread's priority, stack size, creation flags, and security attributes. The complete function prototype is as follows:

CWinThread* AfxBeginThread (AFX_THREADPROC pfnThreadProc, LPVOID pParam, int nPriority = THREAD_PRIORITY_NORMAL, UINT nStackSize = 0, DWORD dwCreateFlags = 0, LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL)

nPriority specifies the thread's execution priority. High-priority threads are scheduled for CPU time before low-priority threads, but in practice, even threads with extremely low priorities usually get all the processor time they need. nPriority doesn't specify an absolute priority level. It specifies a priority level relative to the priority level of the process to which the thread belongs. The default is THREAD_PRIORITY_NORMAL, which assigns the thread the same priority as the process that owns it. You can change a thread's priority level at any time with CWinThread::SetThreadPriority.

The nStackSize parameter passed to AfxBeginThread specifies the thread's maximum stack size. In the Win32 environment, each thread receives its own stack. The 0 default nStackSize value allows the stack to grow as large as 1 MB. This doesn't mean that every thread requires a minimum of 1 MB of memory; it means that each thread is assigned 1 MB of address space in the larger 4-GB address space in which 32-bit Windows applications execute. Memory isn't committed (assigned) to the stack's address space until it's needed, so most thread stacks never use more than a few kilobytes of physical memory. Placing a limit on the stack size allows the operating system to trap runaway functions that recur endlessly and eventually consume the stack. The default limit of 1 MB is fine for almost all applications.

dwCreateFlags can be one of two values. The default value 0 tells the system to start executing the thread immediately. If CREATE_SUSPENDED is specified instead, the thread starts out in a suspended state and doesn't begin running until another thread (usually the thread that created it) calls CWinThread::ResumeThread on the suspended thread, as demonstrated here:

CWinThread* pThread = AfxBeginThread (ThreadFunc, &threadInfo, THREAD_PRIORITY_NORMAL, 0, CREATE_SUSPENDED); pThread->ResumeThread (); // Start the thread

Sometimes it's useful to create a thread but defer its execution until later. The CREATE_SUSPENDED flag is your mechanism for enacting delayed execution.

The final parameter in AfxBeginThread's argument list, lpSecurityAttrs, is a pointer to a SECURITY_ATTRIBUTES structure that specifies the new thread's security attributes and also tells the system whether child processes should inherit the thread handle. The NULL default value assigns the new thread the same properties the thread that created it has.

The Thread Function

A thread function is a callback function, so it must be either a static class member function or a global function declared outside a class. It is prototyped this way:

UINT ThreadFunc (LPVOID pParam)

pParam is a 32-bit value whose value equals the pParam passed to AfxBeginThread. Very often, pParam is the address of an application-defined data structure containing information passed to the worker thread by the thread that created it. It can also be a scalar value, a handle, or even a pointer to an object. Using the same thread function for two or more threads is perfectly legal, but you should be sensitive to reentrancy problems caused by global and static variables. As long as the variables (and objects) a thread uses are created on the stack, no reentrancy problems occur because each thread gets its own stack.

Creating a UI Thread

Creating a UI thread is an altogether different process than creating a worker thread. A worker thread is defined by its thread function, but a UI thread's behavior is governed by a dynamically creatable class derived from CWinThread that resembles an application class derived from CWinApp. The UI thread class shown below creates a top-level frame window that closes itself when clicked with the left mouse button. Closing the window terminates the thread, too, because CWnd::OnNcDestroy posts a WM_QUIT message to the thread's message queue. Posting a WM_QUIT message to a secondary thread ends the thread. Posting a WM_QUIT message to a primary thread ends the thread and ends the application, too.

// The CUIThread class class CUIThread : public CWinThread { DECLARE_DYNCREATE (CUIThread) public: virtual BOOL InitInstance (); }; IMPLEMENT_DYNCREATE (CUIThread, CWinThread) BOOL CUIThread::InitInstance () { m_pMainWnd = new CMainWindow; m_pMainWnd->ShowWindow (SW_SHOW); m_pMainWnd->UpdateWindow (); return TRUE; } // The CMainWindow class class CMainWindow : public CFrameWnd { public: CMainWindow (); protected: afx_msg void OnLButtonDown (UINT, CPoint); DECLARE_MESSAGE_MAP () }; BEGIN_MESSAGE_MAP (CMainWindow, CFrameWnd) ON_WM_LBUTTONDOWN () END_MESSAGE_MAP () CMainWindow::CMainWindow () { Create (NULL, _T ("UI Thread Window")); } void CMainWindow::OnLButtonDown (UINT nFlags, CPoint point) { PostMessage (WM_CLOSE, 0, 0); }

Notice the SW_SHOW parameter passed to ShowWindow in place of the normal m_nCmdShow parameter. m_nCmdShow is a CWinApp data member, so when you create a top-level window from a UI thread, it's up to you to specify the window's initial state.

You launch a CUIThread by calling the form of AfxBeginThread that accepts a CRuntimeClass pointer to the thread class:

CWinThread* pThread = AfxBeginThread (RUNTIME_CLASS (CUIThread));

The UI-thread version of AfxBeginThread accepts the same four optional parameters as the worker-thread version, but it doesn't accept a pParam value. Once started, a UI thread runs asynchronously with respect to the thread that created it.

Suspending and Resuming Threads

A running thread can be suspended with CWinThread::SuspendThread and started again with CWinThread::ResumeThread. A thread can call SuspendThread on itself, or another thread can call SuspendThread for it. However, a suspended thread can't call ResumeThread to wake itself up; someone else must call ResumeThread on its behalf. A suspended thread consumes next to no processor time and imposes essentially zero overhead on the system.

For each thread, Windows maintains a suspend count that's incremented by SuspendThread and decremented by ResumeThread. A thread is scheduled for processor time only when its suspend count is 0. If SuspendThread is called twice in succession, ResumeThread must be called twice also. A thread created without a CREATE_SUSPENDED flag has an initial suspend count of 0. A thread created with a CREATE_SUSPENDED flag begins with a suspend count of 1. Both SuspendThread and ResumeThread return the thread's previous suspend count, so you can make sure a thread gets resumed no matter how high its suspend count is by calling ResumeThread repeatedly until it returns 1. ResumeThread returns 0 if the thread it's called on isn't currently suspended.

Putting Threads to Sleep

A thread can put itself to sleep for a specified period of time by calling the API function ::Sleep. A sleeping thread uses no processor time. The statement

::Sleep (10000);

suspends the current thread for 10 seconds.

One use for ::Sleep is to implement threads whose actions are inherently time-based, such as the background thread in an animation control or a thread that moves the hands of a clock. ::Sleep can also be used to relinquish the remainder of a thread's timeslice. The statement

::Sleep (0);

suspends the current thread and allows the scheduler to run other threads of equal or higher priority. If no other equal or higher priority threads are awaiting execution time, the function call returns immediately and the current thread resumes execution. In Microsoft Windows NT 4.0 and higher, you can yield to another thread by calling ::SwitchToThread. Use ::Sleep (0) if the code you're writing must work on all Win32 platforms.

If you write an application that uses multiple threads to draw to the screen, a few strategically placed ::Sleep (0) statements can do wonders for the quality of the output. Suppose you're animating the motion of four objects and you assign each object its own thread. Each thread is responsible for moving one object across the screen. If you simply run each thread in a loop and allow it to grab for all the processor time it can get, the motion of the objects is likely to be grainy and irregular. But if you have each thread move its assigned object a few pixels and then call ::Sleep (0), the animation can be performed more smoothly.

The value you pass to ::Sleep doesn't guarantee that the thread will be awakened at the precise moment that the time-out interval elapses. Passing ::Sleep a value of 10,000 guarantees that the thread will awaken sometime after 10 seconds have elapsed. The thread might sleep for 10 seconds, or it might sleep for 20—it's all up to the operating system. In practice, the thread will usually begin running again a fraction of a second after the time-out interval elapses, but there are no guarantees. Presently, no method exists in any version of Windows to suspend a thread for a precise amount of time.

Terminating a Thread

Once a thread begins, it can terminate in two ways. A worker thread ends when the thread function executes a return statement or when any function anywhere in the thread calls AfxEndThread. A UI thread terminates when a WM_QUIT message is posted to its message queue or when the thread itself calls AfxEndThread. A thread can post a WM_QUIT message to itself with the API function ::PostQuitMessage. AfxEndThread, ::PostQuitMessage, and return all accept a 32-bit exit code that can be retrieved with ::GetExitCodeThread after the thread has terminated. The following statement copies the exit code of the thread referenced by pThread to dwExitCode:

DWORD dwExitCode; ::GetExitCodeThread (pThread->m_hThread, &dwExitCode);

If called for a thread that's still executing, ::GetExitCodeThread sets dwExitCode equal to STILL_ACTIVE (0x103). In this example, the thread handle passed to ::GetExitCodeThread is retrieved from the m_hThread data member of the CWinThread object encapsulating the thread. Anytime you have a CWinThread and you want to call an API function that requires a thread handle, you can get that handle from m_hThread.

Autodeleting CWinThreads

The two-line code sample in the previous section looks innocent enough, but it's an accident waiting to happen unless you're aware of a peculiar characteristic of CWinThread and take steps to account for it.

You already know that AfxBeginThread creates a CWinThread object and returns its address to the caller. But how does that CWinThread get deleted? So that you don't have to call delete on a CWinThread pointer returned by AfxBeginThread, MFC calls delete on that pointer itself after the thread has terminated. Furthermore, CWinThread's destructor uses the ::CloseHandle API function to close the thread handle. Thread handles must be closed explicitly because they remain open even after the threads associated with them have terminated. They have to remain open; otherwise, functions such as ::GetExitCodeThread couldn't possibly work.

On the surface, the fact that MFC automatically deletes CWinThread objects and closes the corresponding thread handles seems convenient. If MFC didn't handle these routine housekeeping chores for you, you'd have to handle them yourself. But there's a problem—at least a potential one. Look again at this statement:

::GetExitCodeThread (pThread->m_hThread, &dwExitCode);

There's nothing wrong with this code if the thread hasn't terminated, because pThread is still a valid pointer. But if the thread has terminated, it's highly likely that MFC has deleted the CWinThread object and that pThread is now an invalid pointer. (I say "highly likely" because a short window of time separates a thread's termination from the associated CWinThread object's deletion.) An obvious solution is to copy the thread handle from the CWinThread object to a local variable before the thread terminates and to use that handle in the call to ::GetExitCodeThread, like this:

// While the thread is running HANDLE hThread = pThread->m_hThread; // Sometime later ::GetExitCodeThread (hThread, &dwExitCode);

But this code, too, is buggy. Why? Because if the CWinThread object no longer exists, the thread handle no longer exists, either; it has long since been closed. Failure to take into account the autodeleting nature of CWinThreads and the ::CloseHandle call executed by CWinThread's destructor can lead to egregious programming errors if you use functions such as ::GetExitCodeThread that assume a thread's handle is still valid even if the thread is no longer running.

Fortunately, this problem has a solution—two of them, in fact. The first solution is to prevent MFC from deleting a CWinThread object by setting the object's m_bAutoDelete data member equal to FALSE. The default is TRUE, which enables autodeletion. If you choose this route, you must remember to call delete on the CWinThread pointer returned by AfxBeginThread, or your application will suffer memory leaks. The following code fragment illustrates this point:

CWinThread* pThread = AfxBeginThread (ThreadFunc, NULL, THREAD_PRIORITY_NORMAL, 0, CREATE_SUSPENDED); pThread->m_bAutoDelete = FALSE; pThread->ResumeThread (); // Sometime later DWORD dwExitCode; ::GetExitCodeThread (pThread->m_hThread, &dwExitCode); if (dwExitCode == STILL_ACTIVE) { // The thread is still running. } else { // The thread has terminated. Delete the CWinThread object. delete pThread; }

Just as important as deleting the CWinThread object is creating the thread in a suspended state. If you don't, a small but very real chance exists that the new thread will run out its lifetime before the thread that created it executes the statement that sets m_bAutoDelete to FALSE. Remember: Once a thread is started, Windows gives you no guarantees about how much or how little CPU time that thread will be accorded.

The second solution is to allow the CWinThread to autodelete but to use the Win32 ::DuplicateHandle function to create a copy of the thread handle. Thread handles are reference-counted, and using ::DuplicateHandle to duplicate a newly opened thread handle bumps that handle's reference count up from 1 to 2. Consequently, when CWinThread's destructor calls ::CloseHandle, the handle isn't really closed; it simply has its reference count decremented. The downside is that you mustn't forget to call ::CloseHandle yourself to close the handle. Here's an example:

CWinThread* pThread = AfxBeginThread (ThreadFunc, NULL, THREAD_PRIORITY_NORMAL, 0, CREATE_SUSPENDED); HANDLE hThread; ::DuplicateHandle (GetCurrentProcess (), pThread->m_hThread, GetCurrentProcess (), &hThread, 0, FALSE, DUPLICATE_SAME_ACCESS); pThread->ResumeThread (); // Sometime later DWORD dwExitCode; ::GetExitCodeThread (hThread, &dwExitCode); if (dwExitCode == STILL_ACTIVE) { // The thread is still running. } else { // The thread has terminated. Close the thread handle. ::CloseHandle (hThread); }

Once again, the new thread is created in a suspended state so that the creating thread can be absolutely sure to execute code before the new thread ends.

Terminating Another Thread

Generally speaking, threads can terminate only themselves. If you want thread A to terminate thread B, you must set up a signaling mechanism that allows thread A to tell thread B to terminate itself. A simple variable can serve as a termination request flag, as demonstrated here:

// Thread A nContinue = 1; CWinThread* pThread = AfxBeginThread (ThreadFunc, &nContinue); nContinue = 0; // Tell thread B to terminate. // Thread B UINT ThreadFunc (LPVOID pParam) { int* pContinue = (int*) pParam; while (*pContinue) { // Work work work work } return 0; }

In this example, thread B checks nContinue from time to time and terminates if nContinue changes from nonzero to 0. Normally it's not a terrific idea for two threads to access the same variable without synchronizing their actions, but in this case, it's acceptable because thread B is checking only to find out whether nContinue is 0. Of course, to prevent access violations, you need to ensure that nContinue doesn't go out of scope while thread B is running. You can do that by making nContinue a static or global variable.

Now suppose that you want to modify this example so that once thread A sets nContinue to 0, it pauses until thread B is no longer running. Here's the proper way to do it:

// Thread A nContinue = 1; CWinThread* pThread = AfxBeginThread (ThreadFunc, &nContinue); HANDLE hThread = pThread->m_hThread; // Save the thread handle. nContinue = 0; // Tell thread B to terminate. ::WaitForSingleObject (hThread, INFINITE); // Thread B UINT ThreadFunc (LPVOID pParam) { int* pContinue = (int*) pParam; while (*pContinue) { // Work work work work } return 0; }

::WaitForSingleObject blocks the calling thread until the specified object—in this case, another thread—enters a "signaled" state. A thread becomes signaled when it terminates. When a thread blocks in ::WaitForSingleObject, it waits very efficiently because it's effectively suspended until the function call returns. This example assumes that thread B won't end until thread A tells it to. If that's not the case—if thread B could end before thread A commands it to—thread A should create thread B in a suspended state and make a copy of the thread handle with ::DuplicateHandle. Otherwise, thread A could get caught in the trap of passing an invalid thread handle to ::WaitForSingleObject.

::WaitForSingleObject is an indispensable function that you'll use time and time again when writing multithreaded code. The first parameter passed to it is the handle of the object you want to wait on. (It can also be a process handle, the handle of a synchronization object, or a file change notification handle, among other things.) In the example above, thread A retrieves thread B's handle before setting nContinue to 0 because the CWinThread object representing thread B might no longer exist when the call to ::WaitForSingleObject executes. The second parameter to ::WaitForSingleObject is the length of time you're willing to wait. INFINITE means wait as long as it takes. When you specify INFINITE, you take the chance that the calling thread could lock up if the object it's waiting on never becomes signaled. If you specify a number of milliseconds instead, as in

::WaitForSingleObject (hThread, 5000);

::WaitForSingleObject will return after the specified time—here 5 seconds—has elapsed even if the object hasn't become signaled. You can check the return value to determine why the function returned. WAIT_OBJECT_0 means that the object became signaled, and WAIT_TIMEOUT means that it didn't.

Given a thread handle or a valid CWinThread object wrapping a thread handle, you can quickly determine whether the thread is still running by calling ::WaitForSingleObject and specifying 0 for the time-out period, as shown here:

if (::WaitForSingleObject (hThread, 0) == WAIT_OBJECT_0) { // The thread no longer exists. } else { // The thread is still running. }

Called this way, ::WaitForSingleObject doesn't wait; it returns immediately. A return value equal to WAIT_OBJECT_0 means that the thread is signaled (no longer exists), and a return value equal to WAIT_TIMEOUT means that the thread is nonsignaled (still exists). As usual, it's up to you to ensure that the handle you pass to ::WaitForSingleObject is a valid one, either by duplicating the original thread handle or by preventing the CWinThread object from being autodeleted.

There is one way a thread can kill another thread directly, but you should use it only as a last resort. The statement

::TerminateThread (hThread, 0);

terminates the thread whose handle is hThread and assigns it an exit code of 0. The Win32 API reference documents some of the many problems ::TerminateThread can cause, which range from orphaned thread synchronization objects to DLLs that don't get a chance to execute normal thread-shutdown code.

Threads, Processes, and Priorities

The scheduler is the component of the operating system that decides which threads run when and for how long. Thread scheduling is a complex task whose goal is to divide CPU time among multiple threads of execution as efficiently as possible to create the illusion that all of them are running at once. On machines with multiple CPUs, Windows NT and Windows 2000 really do run two or more threads at the same time by assigning different threads to different processors using a scheme called symmetric multiprocessing, or SMP. Windows 95 and Windows 98 are not SMP operating systems, so they schedule all of their threads on the same CPU even on multiprocessor PCs.

The scheduler uses a variety of techniques to improve multitasking performance and to try to ensure that each thread in the system gets an ample amount of CPU time. (For an inside look at the Windows NT scheduler, its strategies, and its algorithms, I highly recommend the book Inside Windows NT, second edition, by David Solomon.) Ultimately, however, the decision about which thread to execute next boils down to the thread with the highest priority. At any given moment, each thread is assigned a priority level from 0 through 31, with higher numbers indicating higher priorities. If a priority-11 thread is waiting to execute and all other threads vying for CPU time have priority levels of 10 or less, the priority-11 thread runs next. If two priority-11 threads are waiting to execute, the scheduler executes the one that has executed the least recently. When that thread's timeslice, or quantum, is up, the other priority-11 thread gets executed if all the other threads still have lower priorities. As a rule, the scheduler always gives the next timeslice to the waiting thread with the highest priority.

Does this mean that low-priority threads never get executed? Not at all. First, remember that Windows is a message-based operating system. If a thread calls ::GetMessage and its message queue is empty, the thread blocks until a message becomes available. This gives lower priority threads a chance to execute. Most UI threads spend the vast majority of their time blocked on the message queue, so as long as a high-priority worker thread doesn't monopolize the CPU, even very low priority threads typically get all the CPU time they need. (A worker thread never blocks on the message queue because it doesn't process messages.)

The scheduler also plays a lot of tricks with priority levels to enhance the overall responsiveness of the system and to reduce the chance that any thread will be starved for CPU time. If a thread with a priority level of 7 goes for too long without receiving a timeslice, the scheduler may temporarily boost the thread's priority level to 8 or 9 or even higher to give it a chance to execute. Windows NT 3.x boosts the priorities of threads that belong to the foreground process to improve the responsiveness of the application in which the user is working, and Windows NT 4.0 Workstation boosts the threads' quantums. Windows also uses a technique called priority inheritance to prevent high-priority threads from blocking for too long on synchronization objects owned by low-priority threads. For example, if a priority-11 thread tries to claim a mutex owned by a priority-5 thread, the scheduler may boost the priority of the priority-5 thread so that the mutex will come free sooner.

How do thread priorities get assigned in the first place? When you call AfxBeginThread or CWinThread::SetThreadPriority, you specify a relative thread priority. The operating system combines the relative priority level with the priority class of the process that owns the thread (more about that in a moment) to compute a base priority level for the thread. The thread's actual priority level—a number from 0 through 31—can vary from moment to moment because of priority boosting and deboosting. You can't control boosting (and you wouldn't want to even if you could), but you can control the base priority level by setting the process priority class and the relative thread priority level.

Process Priority Classes

Most processes begin life with the priority class NORMAL_PRIORITY_CLASS. Once started, however, a process can change its priority by calling ::SetPriorityClass, which accepts a process handle (obtainable with ::GetCurrentProcess) and one of the specifiers shown in the following table.

Process Priority Classes

Priority Class Description
IDLE_PRIORITY_CLASS The process runs only when the system is idle—for example, when no other thread is waiting for a given CPU.
NORMAL_PRIORITY_CLASS The default process priority class. The process has no special scheduling needs.
HIGH_PRIORITY_CLASS The process receives priority over IDLE_PRIORITY_CLASS and NORMAL_PRIORITY_CLASS processes.
REALTIME_PRIORITY_CLASS The process must have the highest possible priority, and its threads should preempt even threads belonging to HIGH_PRIORITY_CLASS processes.

Most applications don't need to change their priority classes. HIGH_PRIORITY_CLASS and REALTIME_PRIORITY_CLASS processes can severely inhibit the responsiveness of the system and can even delay critical system activities such as flushing of the disk cache. One legitimate use of HIGH_PRIORITY_CLASS is for system applications that remain hidden most of the time but pop up a window when a certain input event occurs. These applications impose very little overhead on the system while they're blocked waiting for input, but once the input appears, they receive priority over normal applications. REALTIME_PRIORITY_CLASS is provided primarily for the benefit of real-time data acquisition programs that must have the lion's share of the CPU time in order to work properly. IDLE_PRIORITY_CLASS is ideal for screen savers, system monitors, and other low-priority applications that are designed to operate unobtrusively in the background.

Relative Thread Priorities

The table below shows the relative thread priority values you can pass to AfxBeginThread and CWinThread::SetThreadPriority. The default is THREAD_PRIORITY_NORMAL, which AfxBeginThread automatically assigns to a thread unless you specify otherwise. Normally, a THREAD_PRIORITY_NORMAL thread that belongs to a NORMAL_PRIORITY_CLASS process has a base priority level of 8. At various times, the thread's priority may be boosted for reasons mentioned earlier, but it will eventually return to 8. A THREAD_PRIORITY_LOWEST thread running in a HIGH_PRIORITY_CLASS background or foreground process has a base priority of 11. The actual numbers aren't as important as realizing that you can fine-tune the relative priorities of the threads within a process to achieve the best responsiveness and performance—and if necessary, you can adjust the priority of the process itself.

Relative Thread Priorities

Priority Value Description
THREAD_PRIORITY_IDLE The thread's base priority level is 1 if the process's priority class is HIGH_PRIORITY_CLASS or lower, or 16 if the process's priority class is REALTIME_PRIORITY_CLASS.
THREAD_PRIORITY_LOWEST The thread's base priority level is equal to the process's priority class minus 2.
THREAD_PRIORITY_BELOW_NORMAL The thread's base priority level is equal to the process's priority class minus 1.
THREAD_PRIORITY_NORMAL The default thread priority value. The thread's base priority level is equal to the process's priority class.
THREAD_PRIORITY_ABOVE_NORMAL The thread's base priority level is equal to the process's priority class plus 1.
THREAD_PRIORITY_HIGHEST The thread's base priority level is equal to the process's priority class plus 2.
THREAD_PRIORITY_TIME_CRITICAL The thread's base priority level is 15 if the process's priority class is HIGH_PRIORITY_CLASS or lower, or 31 if the process's priority class is REALTIME_PRIORITY_CLASS.

Now that you understand where thread priorities come from and how they affect the scheduling process, let's talk about how you know when to adjust thread priorities and what values you should assign to them. As a rule, if a high priority is required, it's usually obvious. If it's not obvious that a thread requires a high priority, a normal thread priority will probably do. For most threads, the default THREAD_PRIORITY_NORMAL is just fine. But if you're writing a communications program that uses a dedicated thread to read and buffer data from a serial port, you might miss bytes here and there unless the thread that does the reading and buffering has a relative priority value of THREAD_PRIORITY_HIGHEST or THREAD_PRIORITY_TIME_CRITICAL.

One thing's for sure: if an application is a CPU hog and it's not designed to fulfill a specific purpose, such as performing real-time data acquisition on a PC dedicated to that task, the market will look upon it unfavorably. CPU time is a computer's most precious resource. Use it judiciously, and don't get caught in the trap of bumping up priority levels to make your own application execute 5 percent faster when doing so might subtract 50 percent from the speed and responsiveness of other applications.

Using C Run-Time Functions in Multithreaded Applications

Certain functions in the standard C run-time library pose problems for multithreaded applications. strtok, asctime, and several other C run-time functions use global variables to store intermediate data. If thread A calls one of these functions and thread B preempts thread A and calls the same function, global data stored by thread B could overwrite global data stored by thread A, or vice versa. One solution to this problem is to use thread synchronization objects to serialize access to C run-time functions. But even simple synchronization objects can be expensive in terms of processor time. Therefore, most modern C and C++ compilers come with two versions of the C run-time library: one that's thread-safe (can safely be called by two or more threads) and one that isn't. The thread-safe versions of the run-time library typically don't rely on thread synchronization objects. Instead, they store intermediate values in per-thread data structures.

Visual C++ comes with six versions of the C run-time library. Which one you should choose depends on whether you're compiling a debug build or a release build, whether you want to link with the C run-time library statically or dynamically, and, obviously, whether your application is single-threaded or multithreaded. The following table shows the library names and the corresponding compiler switches.

Visual C++ Versions of the C Run-Time Library

Library Name Application Type Switch
Libc.lib Single-threaded; static linking; release builds /ML
Libcd.lib Single-threaded; static linking; debug builds /MLd
Libcmt.lib Multithreaded; static linking; release builds /MT
Libcmtd.lib Multithreaded; static linking; debug builds /MTd
Msvcrt.lib Single-threaded or multithreaded; dynamic linking; release builds /MD
Msvcrtd.lib Single-threaded or multithreaded; dynamic linking; debug builds /MDd

Libc.lib, Libcd.lib, Libcmt.lib, and Libcmtd.lib are static link libraries containing C run-time code; Msvcrt.lib and Msvcrtd.lib are import libraries that enable an application to dynamically link to functions in the Visual C++ C run-time DLL. Of course, you don't have to fuss with compiler switches unless you build your own make files. If you're using Visual C++, just select the appropriate entry in the Use Run-time Library field of the Project Settings dialog box and the IDE will add the switches for you. Even if you write a multithreaded application that doesn't use C run-time functions, you should link with one of the multithreaded libraries anyway because MFC calls certain C run-time functions itself.

In an MFC application, that's all you have to do to make calls to C run-time functions thread-safe. Simply set the compiler switches, and trust the class library to do the rest. In an SDK application, you must also replace calls to ::CreateThread with calls to _beginthreadex. MFC programmers don't need to worry about _beginthreadex because AfxBeginThread calls it automatically.

Calling MFC Member Functions Across Thread Boundaries

Now for the bad news about writing multithreaded MFC applications. As long as threads don't call member functions belonging to objects created by other threads, there are few restrictions on what they can do. However, if thread A passes a CWnd pointer to thread B and thread B calls a member function of that CWnd object, MFC is likely to assert in a debug build. A release build might work fine—but then again, it might not. There's also the possibility that a debug build won't assert but that it won't work properly, either. It all depends on what happens inside the framework when that particular CWnd member function is called. You can avoid a potential minefield of problems by compartmentalizing your threads and having each thread use only those objects that it creates rather than rely on objects created by other threads. But for cases in which that's simply not practical, here are a few rules to go by.

First, many MFC member functions can be safely called on objects in other threads. Most of the inline functions defined in the INL files in MFC's Include directory can be called across thread boundaries because they are little more than wrappers around API functions. But calling a noninline member function is asking for trouble. For example, the following code, which passes a CWnd pointer named pWnd from thread A to thread B and has B call CWnd::GetParent through the pointer, works without any problems:

CWinThread* pThread = AfxBeginThread (ThreadFunc, pWnd); UINT ThreadFunc (LPVOID pParam) { CWnd* pWnd = (CWnd*) pParam; CWnd* pParent = pWnd->GetParent (); return 0; }

Simply changing GetParent to GetParentFrame, however, causes an assertion:

CWinThread* pThread = AfxBeginThread (ThreadFunc, pWnd); UINT ThreadFunc (LPVOID pParam) { CWnd* pWnd = (CWnd*) pParam; // Get ready for an assertion! CWnd* pParent = pWnd->GetParentFrame (); return 0; }

Why does GetParent work when GetParentFrame doesn't? Because GetParent calls through almost directly to the ::GetParent function in the API. Here's how CWnd::GetParent is defined in Afxwin2.inl, with a little reformatting thrown in to enhance readability:

_AFXWIN_INLINE CWnd* CWnd::GetParent () const { ASSERT (::IsWindow (m_hWnd)); return CWnd::FromHandle (::GetParent (m_hWnd)); }

No problem there; m_hWnd is valid because it's part of the CWnd object that pWnd points to, and FromHandle converts the HWND returned by ::GetParent into a CWnd pointer.

But now consider what happens when you call GetParentFrame, whose source code is found in Wincore.cpp. The line that causes the assertion error is

ASSERT_VALID (this);

ASSERT_VALID calls CWnd::AssertValid, which performs a sanity check by making sure that the HWND associated with this appears in the permanent or temporary handle map the framework uses to convert HWNDs into CWnds. Going from a CWnd to an HWND is easy because the HWND is a data member of the CWnd, but going from an HWND to a CWnd can be done only through the handle maps. And here's the problem: Handle maps are local to each thread and aren't visible to other threads. If thread A created the CWnd whose address is passed to ASSERT_VALID, the corresponding HWND won't appear in thread B's permanent or temporary handle map and MFC will assert. Many of MFC's noninline member functions call ASSERT_VALID, but inline functions don't—at least not in current releases.

Frequently, MFC's assertions protect you from calling functions that wouldn't work anyway. In a release build, GetParentFrame returns NULL when called from a thread other than the one in which the parent frame was created. But in cases in which assertion errors are spurious—that is, in cases in which the function would work okay despite the per-thread handle tables—you can avoid assertions by passing real handles instead of object pointers. For example, it's safe to call CWnd::GetTopLevelParent in a secondary thread if you call FromHandle first to create an entry in the thread's temporary handle map, as shown below.

CWinThread* pThread = AfxBeginThread (ThreadFunc, pWnd->m_hWnd); UINT ThreadFunc (LPVOID pParam) { CWnd* pWnd = CWnd::FromHandle ((HWND) pParam); CWnd* pParent = pWnd->GetTopLevelParent (); return 0; }

That's why the MFC documentation warns that windows, GDI objects, and other objects should be passed between threads using handles instead of pointers. In general, you'll have fewer problems if you pass handles and use FromHandle to re-create objects in the destination threads. But don't take that to mean that just any function will work. It won't.

What about calling member functions belonging to objects created from "pure" MFC classes such as CDocument and CRect—classes that don't wrap HWNDs, HDCs, or other handle types and therefore don't rely on handle maps? Just what you wanted to hear: some work and some don't. There's no problem with this code:

CWinThread* pThread = AfxBeginThread (ThreadFunc, pRect); UINT ThreadFunc (LPVOID pParam) { CRect* pRect = (CRect*) pParam; int nArea = pRect->Width () * pRect->Height (); return 0; }

But this code will assert on you:

CWinThread* pThread = AfxBeginThread (ThreadFunc, pDoc); UINT ThreadFunc (LPVOID pParam) { CDocument* pDoc = pParam; pDoc->UpdateAllViews (NULL); return 0; }

Even some seemingly innocuous functions such as AfxGetMainWnd don't work when they're called from anywhere but the application's primary thread.

The bottom line is that before you go calling member functions on MFC objects created in other threads, you must understand the implications. And the only way to understand the implications is to study the MFC source code to see how a particular member function behaves. Also keep in mind that MFC isn't thread-safe, a subject we'll discuss further later in this chapter. So even if a member function appears to be safe, ask yourself what might happen if thread B accessed an object created by thread A and thread A preempted thread B in the middle of the access.

This stuff is incredibly difficult to sort out and only adds to the complexity of writing multithreaded applications. That's why in the real world, multithreaded MFC applications tend to do the bulk of their user interface work in the main thread. If a background thread wants to update the user interface, it sends or posts a message to the main thread so that the main thread can do the updating. You'll see examples of this kind of messaging in this chapter's sample programs.

Your First Multithreaded Application

The application shown in Figure 17-1 demonstrates some of the basic principles involved in designing and implementing a multithreaded application. Sieve is a dialog-based application that uses the famous Sieve of Eratosthenes algorithm to compute the number of prime numbers between 2 and a number that you specify. The computation begins when you click the Start button and ends when a count appears in the box in the center of the window. Because counting primes is resource-intensive, Sieve does all its counting in a background thread. (To see just how resource-intensive counting primes can be, ask Sieve to count primes between 2 and 100,000,000. Unless your system has gobs of memory, you'll wait a while for the answer.) If the primary thread were to perform the counting, Sieve would be frozen to input for the duration. But because it delegates the task of counting primes to a worker thread, Sieve remains responsive to user input no matter how much time the computation requires.

Figure 17-1. The Sieve window.

The thread that does the counting is launched by the Start button's ON_BN_CLICKED handler, OnStart. You can see the source code yourself in Figure 17-2. Here's the code that launches the thread:

THREADPARMS* ptp = new THREADPARMS; ptp->nMax = nMax; ptp->hWnd = m_hWnd; AfxBeginThread (ThreadFunc, ptp);

OnStart passes data to the worker thread in an application-defined data structure named THREADPARMS. One of the items included in the structure is the upper limit that the user typed into the dialog (nMax). The other is the dialog's window handle. The upper limit is passed to the Sieve function that does the actual counting. The dialog's window handle is used to post a message to the application's main window once the worker thread has arrived at a result:

int nCount = Sieve (nMax); ::PostMessage (hWnd, WM_USER_THREAD_FINISHED, (WPARAM) nCount, 0);

WM_USER_THREAD_FINISHED is a user-defined message ID defined in SieveDlg.h. The main window's WM_USER_THREAD_FINISHED handler retrieves the result from the message's wParam and displays it in the window.

Notice that storage for the THREADPARMS structure passed by address to the thread function is allocated in the primary thread and deallocated in the worker thread, as shown here:

// In the primary thread THREADPARAMS* ptp = new THREADPARMS; AfxBeginThread (ThreadFunc, ptp); // In the worker thread THREADPARMS* ptp = (THREADPARMS*) pParam; delete ptp;

Why create the structure in one thread and delete it in another? Because if you create the structure on the stack in the primary thread, it might go out of scope before the other thread gets a chance to access it. This is one of those annoying little details that can cause seemingly random errors if you don't handle it properly. Allocating the structure with new ensures that scoping problems won't occur, and allocating memory in one thread and deleting it in another isn't harmful. Making the structure a class data member or declaring it globally is an equally effective method of ensuring that it doesn't go away too soon.

When an application's primary thread terminates, the process terminates and any other threads that belong to the process terminate, too. Multithreaded SDK applications typically don't bother to kill background threads before terminating, but MFC applications that end without terminating running background threads suffer memory leaks because the threads' CWinThread objects don't get autodeleted. Such leaks aren't a big deal because the operating system cleans them up almost immediately. However, if you'd rather leave nothing to chance, you can avoid leaking CWinThreads by deleting extant CWinThread objects just before your application shuts down. It's not harmful to delete a running CWinThread, but keep in mind that you can't call CWinThread functions on a deleted CWinThread, either.

Figure 17-2. The Sieve application.

Sieve.h

// Sieve.h : main header file for the SIEVE application // #if !defined(AFX_SIEVE_H__6DF40C9B_7EA1_11D1_8E53_E4D9F9C00000__INCLUDED_) #define AFX_SIEVE_H__6DF40C9B_7EA1_11D1_8E53_E4D9F9C00000__INCLUDED_ #if _MSC_VER >= 1000 #pragma once #endif // _MSC_VER >= 1000 #ifndef __AFXWIN_H__ #error include `stdafx.h' before including this file for PCH #endif #include "resource.h" // main symbols /////////////////////////////////////////////////////////////////////////// // CSieveApp: // See Sieve.cpp for the implementation of this class // class CSieveApp : public CWinApp { public: CSieveApp(); // Overrides // ClassWizard generated virtual function overrides //AFX_VIRTUAL // Implementation //AFX_MSG DECLARE_MESSAGE_MAP() }; /////////////////////////////////////////////////////////////////////////// // // Microsoft Developer Studio will insert additional declarations // immediately before the previous line. #endif // !defined(AFX_SIEVE_H__6DF40C9B_7EA1_11D1_8E53_E4D9F9C00000__INCLUDED_)

Sieve.cpp

// Sieve.cpp : Defines the class behaviors for the application. // #include "stdafx.h" #include "Sieve.h" #include "SieveDlg.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif /////////////////////////////////////////////////////////////////////////// // CSieveApp BEGIN_MESSAGE_MAP(CSieveApp, CWinApp) //AFX_MSG ON_COMMAND(ID_HELP, CWinApp::OnHelp) END_MESSAGE_MAP() /////////////////////////////////////////////////////////////////////////// // CSieveApp construction CSieveApp::CSieveApp() { // TODO: add construction code here, // Place all significant initialization in InitInstance } /////////////////////////////////////////////////////////////////////////// // The one and only CSieveApp object CSieveApp theApp; /////////////////////////////////////////////////////////////////////////// // CSieveApp initialization BOOL CSieveApp::InitInstance() { // Standard initialization // If you are not using these features and wish to reduce the size // of your final executable, you should remove from the following // the specific initialization routines you do not need. CSieveDlg dlg; m_pMainWnd = &dlg; int nResponse = dlg.DoModal(); if (nResponse == IDOK) { // TODO: Place code here to handle when the dialog is // dismissed with OK } else if (nResponse == IDCANCEL) { // TODO: Place code here to handle when the dialog is // dismissed with Cancel } // Since the dialog has been closed, return FALSE so that we exit the // application, rather than start the application's message pump. return FALSE; }

SieveDlg.h

// SieveDlg.h : header file // #if !defined( AFX_SIEVEDLG_H__6DF40C9D_7EA1_11D1_8E53_E4D9F9C00000__INCLUDED_) #define AFX_SIEVEDLG_H__6DF40C9D_7EA1_11D1_8E53_E4D9F9C00000__INCLUDED_ #if _MSC_VER >= 1000 #pragma once #endif // _MSC_VER >= 1000 #define WM_USER_THREAD_FINISHED WM_USER+0x100 UINT ThreadFunc (LPVOID pParam); int Sieve (int nMax); typedef struct tagTHREADPARMS { int nMax; HWND hWnd; } THREADPARMS; /////////////////////////////////////////////////////////////////////////// // CSieveDlg dialog class CSieveDlg : public CDialog { // Construction public: CSieveDlg(CWnd* pParent = NULL); // standard constructor // Dialog Data //{{AFX_DATA(CSieveDlg) enum { IDD = IDD_SIEVE_DIALOG }; // NOTE: the ClassWizard will add data members here //}}AFX_DATA // ClassWizard generated virtual function overrides //AFX_VIRTUAL // Implementation protected: HICON m_hIcon; // Generated message map functions //AFX_MSG afx_msg LONG OnThreadFinished (WPARAM wParam, LPARAM lParam); DECLARE_MESSAGE_MAP() }; // // Microsoft Developer Studio will insert additional declarations // immediately before the previous line. #endif // !defined( // AFX_SIEVEDLG_H__6DF40C9D_7EA1_11D1_8E53_E4D9F9C00000__INCLUDED_)

SieveDlg.cpp

// SieveDlg.cpp : implementation file // #include "stdafx.h" #include "Sieve.h" #include "SieveDlg.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif /////////////////////////////////////////////////////////////////////////// // CSieveDlg dialog CSieveDlg::CSieveDlg(CWnd* pParent /*=NULL*/) : CDialog(CSieveDlg::IDD, pParent) { //AFX_DATA_INIT // Note that LoadIcon does not require a subsequent // DestroyIcon in Win32 m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME); } void CSieveDlg::DoDataExchange(CDataExchange* pDX) { CDialog::DoDataExchange(pDX); //AFX_DATA_MAP } BEGIN_MESSAGE_MAP(CSieveDlg, CDialog) //AFX_MSG_MAP ON_MESSAGE (WM_USER_THREAD_FINISHED, OnThreadFinished) END_MESSAGE_MAP() /////////////////////////////////////////////////////////////////////////// // CSieveDlg message handlers BOOL CSieveDlg::OnInitDialog() { CDialog::OnInitDialog(); SetIcon(m_hIcon, TRUE); SetIcon(m_hIcon, FALSE); return TRUE; } void CSieveDlg::OnStart() { int nMax = GetDlgItemInt (IDC_MAX); if (nMax < 10) { MessageBox (_T ("The number you enter must be 10 or higher")); GetDlgItem (IDC_MAX)->SetFocus (); return; } SetDlgItemText (IDC_RESULT, _T ("")); GetDlgItem (IDC_START)->EnableWindow (FALSE); THREADPARMS* ptp = new THREADPARMS; ptp->nMax = nMax; ptp->hWnd = m_hWnd; AfxBeginThread (ThreadFunc, ptp); } LONG CSieveDlg::OnThreadFinished (WPARAM wParam, LPARAM lParam) { SetDlgItemInt (IDC_RESULT, (int) wParam); GetDlgItem (IDC_START)->EnableWindow (TRUE); return 0; } /////////////////////////////////////////////////////////////////////////// // Global functions UINT ThreadFunc (LPVOID pParam) { THREADPARMS* ptp = (THREADPARMS*) pParam; int nMax = ptp->nMax; HWND hWnd = ptp->hWnd; delete ptp; int nCount = Sieve (nMax); ::PostMessage (hWnd, WM_USER_THREAD_FINISHED, (WPARAM) nCount, 0); return 0; } int Sieve(int nMax) { PBYTE pBuffer = new BYTE[nMax + 1]; ::FillMemory (pBuffer, nMax + 1, 1); int nLimit = 2; while (nLimit * nLimit < nMax) nLimit++; for (int i=2; i<=nLimit; i++) { if (pBuffer[i]) { for (int k=i + i; k<=nMax; k+=i) pBuffer[k] = 0; } } int nCount = 0; for (i=2; i<=nMax; i++) if (pBuffer[i]) nCount++; delete[] pBuffer; return nCount; }

Категории