- Table of Contents
- Copyright
- About the Authors
- About the Contributors
- Acknowledgments
- Tell Us What You Think!
- Introduction
- How to Use This Book
- What You Need to Use This Book
- What's New in Visual C++ 6.0
- Contacting the Main Author
- Part I: Introduction
- Chapter 1. The Visual C++ 6.0 Environment
- Part II: MFC Programming
- Chapter 2. MFC Class Library Overview
- Chapter 3. MFC Message Handling Mechanism
- Chapter 4. The Document View Architecture
- Chapter 5. Creating and Using Dialog Boxes
- Chapter 6. Working with Device Contexts and GDI Objects
- Chapter 7. Creating and Using Property Sheets
- Chapter 8. Working with the File System
- Chapter 9. Using Serialization with File and Archive Objects
- Part III: Internet Programming with MFC
- Chapter 10. MFC and the Internet Server API (ISAPI)
- Chapter 11. The WinInet API
- Chapter 12. MFC HTML Support
- Part IV: Advanced Programming Topics
- Chapter 13. Using the Standard C++ Library
- Chapter 14. Error Detection and Exception Handling Techniques
- Chapter 15. Debugging and Profiling Strategies
- Chapter 16. Multithreading
- Chapter 17. Using Scripting and Other Tools to Automate the Visual C++ IDE
- Part V: Database Programming
- Chapter 18. Creating Custom AppWizards
- Chapter 19. Database Overview
- Chapter 20. ODBC Programming
- Chapter 21. MFC Database Classes
- Chapter 22. Using OLE DB
- Chapter 23. Programming with ADO
- Part VI: MFC Support for COM and ActiveX
- Chapter 24. Overview of COM and Active Technologies
- Chapter 25. Active Documents
- Chapter 26. Active Containers
- Chapter 27. Active Servers
- Chapter 28. ActiveX Controls
- Part VII: Using the Active Template Library
- Chapter 29. ATL Architecture
- Chapter 30. Creating COM Objects Using ATL
- Chapter 31. Creating ActiveX Controls Using ATL
- Chapter 32. Using ATL to Create MTS and COM+ Components
- Part VIII: Finishing Touches
- Chapter 33. Adding Windows Help
- Part IX: Appendix
Thread Synchronization
Many developers, when they first learn how to create multiple threads in an application, have a tendency to go overboard and try to use a separate thread for everything an application does. This is not generally a good practice. Not only is it more work to create all those threads, but the effort involved in making sure that all threads cooperate may easily increase exponentially with the number of threads.
I don't mean to scare you away from using threads: They are extremely useful—even necessary—in many situations. Creating a multithreaded application that works correctly is not a trivial task, however. You must give very careful consideration to how your threads will communicate with each other and how they will keep from stomping on each other's data.
You must keep in mind that threads in Win32 may (and will) be preempted by the operating system. That is, any thread may be stopped right where it is and another thread allowed to run for a while. Thus, it is safest to assume that all threads are running simultaneously. Even though only one thread at a time actually is using a CPU, there is no way to know when or where a thread will be preempted, or what other threads will do before the original thread resumes execution. On machines with multiple processors, this is not just a safe assumption—several threads actually are running at the same time.
Potential Pitfalls
For an example of how problems can occur, suppose that you have a linked list that uses pointers to dynamically allocated memory. Also assume that you have thread A, which adds items to a list, and thread B, which deletes items from a list. If thread B is in the middle of deleting an item from a list when the thread is preempted, and then thread A tries to add an item, thread A is likely to run into an item that is only half deleted, perhaps involving pointers off into the boonies.
It is not hard to see that this process can cause real problems. This sort of problem is much more difficult to debug, because the problems are dependent on the timing of when threads are preempted. Dealing with bugs that cannot be duplicated makes ditch digging suddenly seem like a much more viable career option.
A similar, but perhaps less obvious, problem can arise even with simple data types. Suppose that one of your threads uses code such as this:
if(nCount == 0)
nCount++;
It is quite possible, if not inevitable, that the thread will be interrupted between testing nCount and incrementing it. This provides a window of opportunity for other threads to modify nCount, perhaps setting it to something other than 0. However, when the thread containing this code resumes, it will increment nCount anyway.
It is certainly much more enjoyable to spend the time to design your applications to avoid these problems than it is to try to find the cause of one of these "gotchas" after you have 100,000 lines of code running at sites around the world.
Now that you've seen just how multithreaded applications can go astray, let's see how you can use MFC's thread-synchronization mechanisms to help your threads play well with others.
CCriticalSection
In places where you know that your threads will be dealing with things that only one thread at a time should be accessing, you can use critical sections to ensure that only one thread can access certain pieces of code at the same time.
To use MFC's critical sections, you first need to create a CCriticalSection object. Because the constructor takes no arguments, this step is trivial.
Next, before a thread in your application needs to enter a critical section of the code, it should call the Lock() member of your CCriticalSection object. If no other threads have the critical section locked, Lock() locks the critical section and returns, allowing the calling thread to continue into the critical section and manipulate data as it sees fit. If a second thread tries to lock the same critical section object, the Lock() call blocks the thread until the critical section is available. This occurs when the first thread calls Unlock(), allowing other threads to access the critical section.
CCriticalSection provides a simple, lightweight mechanism to limit access to critical sections in your code; however, it does have its limitations.
First, using the CCriticalSection object is purely voluntary on the part of the developer. If you don't use the critical section properly, it won't do anything to protect your data. It is your responsibility to correctly and consistently use critical sections to control access to all data that might be corrupted by simultaneous access by multiple threads. You are responsible for calling Lock() before each bit of code that might cause problems. You also need to make certain to call Unlock() when you are finished working with the sensitive data; otherwise, the other processes will wait forever on the Lock() call.
Second, CCriticalSection objects are valid only within a process; you cannot use them to protect memory or other resources shared between processes. For this, you need to use one of the beefier classes, such as CMutex.
Finally, the performance advantage gained with critical sections tends to be reduced when many threads are contending for a single critical section. Also, critical sections incur slight additional overhead when used on multiprocessor machines.
CMutex
The MFC CMutex class is similar to CCriticalSection in that you can use it to provide mutually exclusive access to certain sections of your code. Like critical sections, CMutex objects work to protect your data only if you use them, although you can use them between different processes. Only one thread may own a given mutex at a given time; all others will block on a call to Lock() until the owning thread releases ownership.
First, you create a CMutex object. The prototype for its constructor looks like this:
CMutex( BOOL bInitiallyOwn = FALSE,
LPCTSTR lpszName = NULL,
LPSECURITY_ATTRIBUTES lpsaAttribute = NULL );
The first parameter enables you to specify the ownership of the mutex when it is created. It often is useful to declare a mutex object as a member of a class representing data that needs to be protected from simultaneous access by multiple threads. In cases like this, you can initialize the mutex and lock it while the protected data is initialized; then, you can unlock the mutex when it is safe for other threads to start accessing the data.
In addition, you may specify a name for your mutex. This is not necessary if the mutex will be used in only one process, but it is the only way you will be able to share a mutex between processes.
Under Windows NT, you may specify the security attributes for the mutex in the last parameter. Windows 95 and Windows 98 simply ignore this parameter.
You use the Lock() and Unlock() functions in much the same way you use the calls for critical sections, except that Lock() uses the timeout value passed as a parameter. If the mutex is unavailable when Lock() is called, the call blocks for the specified number of milliseconds, waiting for the mutex to become available, at which time Lock() returns TRUE. If the timeout time elapses before the mutex becomes available, Lock() returns FALSE, and your code should react accordingly.
To share a mutex between two processes, you should create a CMutex object in each process, making certain to use the same name in the call to the constructor. The first thread to call the constructor actually creates the operating system mutex, as well as the CMutex object.
The next thread to call CMutex() creates a CMutex object corresponding to the mutex object that already was created with the same name. If the bInitallyOwn parameter is set to TRUE, the call to CMutex() does not return until the mutex becomes available. You can take advantage of this fact to create a CMutex only when you are ready to wait on it, although you will not be able to specify a timeout value using this method.
CSemaphore
Semaphore objects are similar to mutexes, except instead of providing access to a single thread, you may create semaphores that allow access only to a limited number of threads simultaneously. Semaphores are based on a counter that they keep. When a thread is granted access to the semaphore, this count is decremented. If the count is 0, threads requesting access have to wait until a thread releases the semaphore, thereby increment ing the semaphore count.
The prototype of the constructor for CSemaphore objects looks like this:
CSemaphore( LONG lInitialCount = 1,
LONG lMaxCount = 1,
LPCTSTR pstrName = NULL,
LPSECURITY_ATTRIBUTES lpsaAttributes = NULL );
The pstrName and lpsaAttributes parameters work just as they did in CMutex, but you have not seen the first two parameters before. These parameters specify an initial value for the semaphore's counter, as well as a maximum value for the counter. The default values of 1 and 1 create a semaphore that is essentially the same as a mutex, because only one thread may be granted access to the semaphore at any one time. You need to modify these parameters when the semaphore is created to allow more than one thread to have access to the semaphore.
The Lock() and Unlock() functions of CSemaphore work the same as those for CMutex, except that CSemaphore may allow a fixed number of concurrent accesses, and CMutex allows only one.
CEvent
Although the previous synchronization objects provide a method of protecting various resources or sections of your code, occasionally, you will want your program to wait for some event to occur instead of waiting for a resource to become available. This method is useful when waiting to receive packets from a network or waiting for a thread to signal that it has completed some task.
To allow you to signal events, Win32 provides the aptly named event object, which MFC encapsulates in the CEvent class. An event object is always in one of two states: signaled or unsignaled (or set or reset). It is common to create an event object in its unsignaled state, and then set it to signaled when an event occurs.
Creation of a CEvent object begins with a call to its constructor:
CEvent( BOOL bInitiallyOwn = FALSE,
BOOL bManualReset = FALSE,
LPCTSTR lpszName = NULL,
LPSECURITY_ATTRIBUTES lpsaAttribute = NULL );
This is very similar to the constructor for the other CSyncObject-derived objects you have seen so far. The parameters passed to the constructor include a name for the object, which may be used by other processes, and a pointer to a security attributes structure. Although an event is not really owned by any one thread (as a mutex is), the bInitiallyOwn parameter enables you to set the initial value of the event. If this value is FALSE, the event initially is unsignaled; if TRUE, the event is set to signaled on creation.
The bManualReset parameter is a bit more interesting, because it dictates how the event object behaves. In general, if this parameter is TRUE, the event must be reset manually by your code after it is signaled. If this is FALSE, the event object resets itself. You will see how this works a bit more clearly as you learn how to manipulate the event.
As you can with other CSyncObjects, you can wait on an event with the Lock() member, which enables you to specify a timeout value. If the wait times out, Lock() returns FALSE. If the event is signaled before the timeout, Lock() returns TRUE.
Signaling an Event
As mentioned previously, an event can be signaled when it is first created, but this generally is not done. To signal an event, you can call SetEvent().
If bManualReset for the event is FALSE, only one thread that is waiting on the event can proceed (by returning TRUE from the thread's call to Lock()). The event then is reset before any other threads will return from Lock(). If no threads are waiting on the event when SetEvent() is called, the event remains signaled until one thread is given a lock on it. The event then is set to unsignaled automatically.
If, on the other hand, bManualReset is TRUE, all threads that are waiting on the event are allowed to continue (Lock() returns TRUE). The event is not reset automatically, so any subsequent calls to Lock() for the event do not have to wait, but return TRUE. To manually reset the event, you can call ResetEvent(), thus making any further calls to Lock() the event wait until the event is signaled again.
In addition, the PulseEvent() member of CEvent may be used to signal an event. If bManualReset is TRUE, all threads that currently are waiting for the event acquire a lock, and the event is reset automatically.
If bManualReset is FALSE, one waiting thread can acquire a lock before the event is reset. Note that if no threads currently are waiting on the event, the event is reset immediately. Unlike SetEvent(), PulseEvent() does not keep the event signaled until a thread acquires the lock.
CSingleLock
MFC provides classes that can simplify access to the synchronization objects discussed previously. These classes include CMultipleLock, which allows threads to wait for combinations of objects (as you will see next), and CSingleLock, which you can use to work with single synchronization objects.
In most cases, you will not really need to use CSingleLock, despite the Microsoft documentation that says you must. The Lock() and Unlock() functions work just like the Lock() and Unlock() calls in the underlying CSyncObjects.
The difference is that CSingleLock can provide the IsLocked() function, which can tell you whether your thread will be able to acquire a lock without actually locking the object. Keep in mind that the state of a sync object may change between a call to IsLocked() and the actual Lock() call.
Microsoft also justifies the CSingleLock class as an excuse to be sloppy in your coding habits. If a CSingleLock object falls out of scope and is destroyed, the destructor calls Unlock() for you.
As you probably can guess, I am not sold on the CSingleLock class. It provides very little useful functionality for the extra code it requires and just muddles your source code. On the other hand, the CMultiLock class is extremely useful, if not essential, in writing complex multithreaded applications.
CMultiLock
In complex multithreaded applications, you often will need to acquire several different shared resources to perform an operation, or you may want to wait for one of several different events to be signaled. With what you have seen so far in this chapter, you can do such things in your applications, but it isn't easy.
To help you work with multiple synchronization objects, MFC provides the CMultipleLock class, which can greatly simplify operations, such as waiting for several different resources or a set of different events. The prototype for the CMultiLock constructor shows that it accepts an array of CSyncObject pointers and the size of the array:
CMultiLock( CSyncObject* ppObjects[ ],
DWORD dwCount,
BOOL bInitialLock = FALSE );
In addition, you may specify bInitialLock to acquire a lock on the objects specified in the ppObjects array when the CMultiLock is created. However, you probably will want to hold off on any locking until later, when Lock() allows some other special capabilities.
CMultiLock::Lock()
Although you may be interested in using the IsLocked() member to query the status of an individual object handled with the CMultiLock, the real purpose for CMultiLock is its Lock() function, as shown in this prototype:
DWORD Lock( DWORD dwTimeOut = INFINITE,
BOOL bWaitForAll = TRUE,
DWORD dwWakeMask = 0 );
The timeout parameter should seem familiar by now. Like the other Lock() calls you have seen, this call can be used to specify a maximum time to wait for a lock (in milliseconds). You can use the special value of INFINITE to wait until you shut down the machine to install the latest, greatest Win32 operating system.
If the bWaitForAll parameter is TRUE, Lock() does not return successfully until all the objects handled in CMultiLock are available. This is useful when you need to lock several resources at one time, and it can go a long way toward preventing deadlocks. If bWaitForAll is FALSE, Lock() returns whenever any one object is locked. This is handy when waiting for several different events, and you want to wait for any one to occur, but don't expect all of them to occur.
In addition to the sync objects specified in the constructor for CMultiLock—the Lock() function—can use the dwWakeMask parameter to specify several Windows messages that cause Lock() to return.
Although your thread will not receive any messages while it is blocked in a call to Lock(), MFC checks the messages that have been sent to your thread to see whether the Lock() should return.
Table 16.1 lists the flags you may specify in dwWakeMask.
Table 16.1. dwWakeMask Flags
| Flag | Message Type That Will Interrupt Lock() |
| QS_ALLEVENTS | Any message, other than those covered by QS_SENDMESSAGE |
| QS_ALLINPUT | Any message at all |
| QS_HOTKEY | A WM_HOTKEY message |
| QS_INPUT | A user input message (either QS_KEY or QS_MOUSE) |
| QS_KEY | A keyboard message; includes WM_KEYUP, WM_KEYDOWN, WM_SYSKEYUP, and WM_SYSKEYDOWN |
| QS_MOUSE | Any mouse movement or button message, such as WM_MOUSEMOVE, WM_LBUTTONDOWN, WM_RBUTTONUP, and so on |
| QS_MOUSEBUTTON | A mouse button message, such as WM_LBUTTONDOWN, WM_RBUTTONUP, and so on |
| QS_MOUSEMOVE | A WM_MOUSEMOVE message |
| QS_PAINT | WM_PAINT or related messages, such as WM_NCPAINT |
| QS_POSTMESSAGE | Any posted message not included in the preceding categories. |
| QS_SENDMESSAGE | Any message sent by another thread or application |
| QS_TIMER | A WM_TIMER message |
Okay, so Lock() can return for 101 reasons, but how do you know what caused the return?
CMultiLock::Lock() Return Values
To see what caused Lock() to stop waiting, look at the DWORD that Lock() returns.
If Lock() returns a value between WAIT_OBJECT_0 and WAIT_OBJECT_0 + (dwCount -1), one of the sync objects specified has been locked. You can get the index by subtracting WAIT_OBJECT_0 from the return value. Of course, this is if bWaitForAll is FALSE. If bWaitForAll is TRUE, a value in this range signifies that all the requested sync objects have been locked.
If Lock() returns WAIT_OBJECT_0 + dwCount, a message specified in dwWakeMask is available on the message queue. (Note that the dwCount I use here refers to the number of objects specified in the CMultiLock constructor.)
If Lock() returns a value between WAIT_ABANDONED_0 and WAIT_ABANDONED_0 + (dwCount - 1), one of the objects has been abandoned. This occurs when a process that owns the thread that owns a sync object has died without first releasing the sync object. If bWaitForAll is TRUE, you receive this return value, but not until all other objects specified in CMultiLock are locked or abandoned.
Finally, if Lock() returns WAIT_TIMEOUT, you probably can guess that the timeout time has expired.
Cleaning Up
When you are finished with the sync objects you have locked, you should call Unlock() to unlock all objects used by CMultiLock. Although Unlock() is called for you when CMultiLock is destroyed, it is a good practice to call Unlock() explicitly, as soon as you are finished with the resources protected by sync objects.
Creating a New Process | Next Section

Account Sign In
View your cart