Documente online.
Zona de administrare documente. Fisierele tale
Am uitat parola x Creaza cont nou
 HomeExploreaza
upload
Upload




Multithreading with MFC

visual c en


Multithreading with MFC

The Win32 operating systems support multiple processes, which are given their own memory address space, as you saw in Chapter 8, 'Memory Management.' In addition, Win32 supports multiple threads within a process. When any application (or process) starts, it has one primary thread. The application may then start up additional threads, which execute independently. All threads share the one memory space of the process in which they are created.



In this chapter, you will see how you can create your own multithreaded applications with MFC, including the following:

  • How to enable multithreading in your applications
  • How to create worker threads for background 242c25c processing
  • How to create user-interface threads
  • How to synchronize your threads

Using Multiple Threads in Your Application

If you have created an MFC application with AppWizard, your application is already set up to handle multiple threads. If not, you have to be certain to link with the multithreaded libraries, which can be set up in the C/C++ | Code Generation page of the Project Settings dialog box. This is generally a good thing to do anyway, because there are few situations where your application will require the single-threaded libraries; you also need to use the multithreaded varieties if you plan to use MFC.

MFC Objects and Threads

When an application has multiple threads, the order in which they are executed is random. Because of this, special care must be taken to ensure that two different threads are not working with certain objects at the same time. You will see how to do this later; for now, keep in mind that the MFC classes may be used by more than one thread at a time, but individual objects may not be used by multiple threads simultaneously. For example, two threads can work with their own CString objects at the same time, but if two threads access the same CString, the results are unpredictable at best.

If you will be using MFC objects in your threads, you must create the thread by using a CWinThread object, as you will see here. Threads that are created without using a CWinThread object will not properly initialize the internal variables that MFC needs to work with multiple threads. For example, if you create a thread directly with _beginthreadex(), the resulting thread will not be able to use MFC objects or other MFC functions.

In addition, although threads share the same memory space, they do not share the same structures that MFC uses to map C++ objects to Windows handles. In general, this means that a thread may access only MFC objects created in that thread. There are, however, ways around this that you will look at later.

Types of Threads

MFC implements two types of threads. Although both types use the same underlying Win32 API thread mechanisms and both make use of CWinThread, they are different in the way that MFC adds functionality to the thread.

If you are interested in creating a thread that simply goes off on its own and does something, such as background calculations, without interfacing with the user, you use what MFC calls a worker thread. These threads are based on CWinThread, but you do not need to explicitly create a CWinThread object, because the AfxBeginThread() call will create one for you.

If, on the other hand, you want to create a thread that will deal with parts of a user interface, you create a user-interface thread. MFC will add a message pump to these threads, providing a message loop that is separate from the main message loop of your application's CWinApp object. Actually, CWinApp is itself a prime example of a user-interface thread, because it is derived from CWinThread. Just as MFC derives CWinApp from CWinThread, you create your own class derived from CWinThread to implement user-interface threads.

Working with Worker Threads

Worker threads are handy for any time you want to do something such as calculations or background printing. They are also useful in cases where you need to wait on an event to occur, such as receiving data from another application, without forcing the user to wait. Let's face it—most users are not known for their patience.

Creating a worker thread is relatively simple; the hard part comes later, when you need to make sure that your thread plays well with others—but more on that later. To get a worker thread up and running, you implement a function that will be run in the thread, then create the thread with AfxBeginThread(). Although you may choose to create your own CWinThread-based class, this is not necessary for worker threads.

Starting the Thread

An MFC thread, whether a worker or user-interface thread, is started with a call to AfxBeginThread(). This function is overloaded to handle the creation of the two flavors of threads, but for now, let's look at the variety used to create worker threads. Here is the prototype for this function:

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

The first parameter is a pointer to the function that will be run inside the thread. As you will soon see, this function can take a single parameter, which is passed as the second parameter. Generally, this is a pointer to a data structure of some sort.

Each thread also may have its own priority. This parameter may be set to any of the values accepted by SetThreadPriority(), which is discussed later.

Because each thread executes independently, it must have its own stack to keep track of function calls and the like. The size of the stack may be specified in the call to AfxBeginThread().

In most cases, you will probably want your thread to start doing its thing right off the bat. However, you may specify the CREATE_SUSPENDED flag in the dwCreateFlags parameter to create a thread that is suspended upon creation. This thread will not begin executing until ResumeThread() is called.

Optionally, you may also specify a SECURITY_ATTRIBUTES structure to specify security parameters to be used with the thread.

AfxBeginThread() returns a pointer to the newly created CWinThread object. You squirrel this away somewhere so that you can work with the member functions of CWinThread later.

When you call AfxBeginThread(), it will create a new CWinThread object for you and call its CreateThread() member function. At this point, unless you have specified CREATE_SUSPENDED, your new thread will start executing the function you specified, and the thread that called AfxBeginThread() will go on its merry way.

The new thread will continue to execute the function specified until that function returns, or until you call AfxEndThread() from within the thread. The thread will also terminate if the process it is running in terminates.

Implementing a Thread Function

The sole purpose in life for a worker bee is to make honey. The sole purpose in life for a worker thread is to run its thread function, or controlling function, as it is called in Microsoft's documentation. In general, when the thread starts, this function starts. When the function dies, the thread dies.

First, your thread function should have a prototype that looks like this:

UINT MyThreadProc(LPVOID pParam);

All thread functions take a single 32-bit argument. Although you could pass a single value here, such as an int, it is generally more useful to pass a pointer to a structure or other object that can hold more information. This structure may also be used to return information to the rest of your application.

For example, the following simple thread function could be used to encrypt a string:

UINT MyThreadProc(LPVOID pParam)

This function could be used in a thread created like this:

AfxBeginThread(MyThreadProc, pMySecretString);

Once the thread is created, it will start executing until it either discovers that the pointer passed to it is null, or it finishes with the string. In either case, whether the function calls AfxEndThread() or simply returns, the function stops executing, its stack and other resources are deallocated, and the CWinThread object is deleted.

Accessing a Thread's Return Code

The exit code specified when the function returns or calls AfxEndThread() may be accessed by other threads in your application with a call to ::GetExitCodeThread(), which takes a handle to the thread and a pointer to a DWORD that will receive the exit code. The handle to the thread is contained in the m_hThread member of CWinThread, so it should be no problem to pass this to ::GetExitCodeThread(), right?

Well, there's a catch: By default, the CWinThread object is deleted as soon as the function returns or calls AfxEndThread(). You can get around this in one of two ways.

First, you can set the m_bAutoDelete member of CWinThread to FALSE, which prevents MFC from deleting the object automatically. You can then access the m_hThread member after the thread terminates. However, you are now responsible for deleting the CWinThread object.

Alternatively, you can use ::DuplicateHandle() to create a copy of the m_hThread member once the thread is created. However, you must be certain that you copy the handle before the thread terminates. The only way to be absolutely certain of this is to specify CREATE_SUSPENDED when the thread is created, copy the handle, then call ResumeThread() to start the thread. As you can see, this gets to be a bit involved; thus, it is generally preferable to change the m_bAutoDelete member.

The exit code value that is returned by GetExitCodeThread() will contain STILL_ACTIVE if the thread is still running, or, if the thread has terminated, the return code that the thread passed when it returned or called AfxEndThread(). Note that STILL_ACTIVE is defined as 0x103 (259 decimal), so avoid using this as a return code from your thread.

User-Interface Threads

User-interface threads are similar to worker threads, because they use the same mechanisms provided by the operating system to manage the new thread. However, user-interface threads provide additional functionality from MFC that allows you to use them to handle user-interface objects, such as dialog boxes or windows. To use this functionality, you will have to do a bit more work than you did with worker threads, but this is still much simpler than using the Win32 API directly to set up a new thread to handle windows messages.

Creating the Thread

To create a user-interface thread, you will use a slightly different version of the AfxBeginThread() function. The version used to create user-interface threads takes a pointer to a CRuntimeClass object for your thread class. MFC will create this for you with the RUNTIME_CLASS macro:

AfxBeginThread(RUNTIME_CLASS(CMyThread));

If you want to keep track of the MFC object for your thread, use this:

(CMyThread*) pMyThread = (CMyThread*) AfxBeginThread(RUNTIME_CLASS(CMyThread));

Keeping a pointer to the MFC thread object will allow you to access the member data and functions of your thread class from other threads.

Additionally, you may specify a priority, stack size, and security attributes, as well as the CREATE_SUSPENDED flag for the new thread in the call to AfxBeginThread().

Creating a Thread Class

As mentioned earlier, you derive your own class from CWinThread in order to create an MFC user-interface thread. The class you derive from CWinThread must include its own override for InitInstance(), but you may choose whether to override several other functions or use the defaults provided by CWinThread. You also need to make sure that you use the DECLARE_DYNCREATE and IMPLEMENT_DYNCREATE macros in the declaration and implementation of your thread class.

Initializing New Threads

Whenever a new thread is created from your thread class by calling AfxBeginThread(), MFC will call the InitInstance() function of your thread class. This is the one function that you must override in your thread class. In general, you will want to do any initialization of your thread here, as well as allocate any dynamic memory you know you will need.

However, all that your InitInstance() function really must do is return TRUE so that MFC knows the initialization succeeded and that it should proceed. If something goes wrong in InitInstance(), such as memory allocation failures, it should return FALSE. In this case, execution of the thread stops and the thread is destroyed.

If this thread is designed to handle a window, this is a good place to create the window. You should then set the m_pMainWnd member of CWinThread to a pointer to the window you have created, which will allow the message dispatch system of CWinThread to manage messages from this window exclusively.

NOTE

If you create an MFC window object in a thread, that MFC object cannot be used in another thread, but the handle to the underlying window can. If you wish to work with the window in another thread using an MFC object, the second thread should create a new MFC object; then call Attach() to attach the new object to the window handle passed from the first thread.

If you have allocated memory in your InitInstance() function, or done anything else in the thread that needs cleaning up, you should also override the ExitInstance() member of CWinThread in your thread class. This is called whenever your thread terminates, including cases where InitInstance() returns FALSE.

In addition, you can override the OnIdle() member to perform tasks when the message queue is empty, as you saw in Chapter 3, 'MFC Messages and Commands.' Your thread class can also override ProcessWndProcException(), which is called to handle exceptions that have not been handled elsewhere. The default implementation will handle exceptions caused by only the WM_CREATE or WM_PAINT messages.

Handling Messages in Threads

The default implementation of CWinThread::Run() will provide a message pump for your new thread. You can override this function to do anything else you would like your thread to do, although most user-interface threads won't do this.

In addition, you may override the PreTranslateMessage() function if you want to intercept messages before the message pump dispatches them, although most messages can be handled in the message map.

Your thread class may implement a message map just as for any other class derived from CCmdTarget, as you saw in Chapter 3. However, you may also use a special message map macro, ON_THREAD_MESSAGE, to handle messages that are sent directly to the thread rather than to a given window. You can send messages directly to a thread with CwinThread::PostThreadMessage(), as shown here:

pMyThread->PostThreadMessage(WM_USER+1, myWParam, myLParam);

This is similar to the ::PostThreadMessage() API call, which takes an additional parameter—the thread identifier.

Note that the first parameter specifies the message to post. This example uses WM_USER+1, which gives a valid user-defined message ID. To then handle the message you posted, the message map for your thread would look something like this:

BEGIN_MESSAGE_MAP(CMyThread, CWinThread)
//}AFX_MSG_MAP
ON_THREAD_MESSAGE(WM_USER+1, HandleThreadMessage)
END_MESSAGE_MAP()

This will result in messages sent directly to your thread, which have command ID WM_USER+1, to be handled by the HandleThreadMessage() member of your thread class. The handler for your thread message looks like this:

afx_msg void CMyThread::HandleThreadMessage(WPARAM wParam, LPARAM lParam)

In this case, you decide to use the WM_USER+1 message to tell the thread to exit, although you may choose to implement more elaborate handlers that make use of the wParam and lParam values that are passed from PostThreadMessage().

Terminating Threads

A thread can terminate in several different ways. Normally, a thread will end when the thread function it is running returns. For worker threads, this is pretty straightforward. However, for user-interface threads, you generally don't deal with the thread function directly.

The Run() member of CWinThread is actually the thread function, which, by default, MFC uses to implement a message pump for the thread. This function will exit upon receiving a WM_QUIT message, which a thread may send to itself with a call to PostQuitMessage(), as in the previous example.

Additionally, if your user-interface thread manages an MFC window, as set in the m_pMainWnd member, MFC will call PostQuitMessage() for you when the main window is destroyed. In either case, user-interface threads will call ExitInstance() before actually terminating.

The Win32 API provides the TerminateThread() function, but using this method can have some very dire consequences. Unlike the previous methods of terminating threads gracefully, TerminateThread() stops the thread dead in its tracks, without any provisions for cleaning up allocated memory. More importantly, the thread that is terminated in this way may be interrupted in the middle of several different transactions (such as device driver communications or heap management) that will leave the system in an indeterminate state. In Windows 95, this equates to almost certain doom for the whole system, whereas Windows NT should be able to at least save the rest of the processes. In either OS, the offending process is a goner.

If you need to terminate a thread from outside that thread, try to use some form of communications to tell the thread to terminate itself gracefully. The messages used in the previous example work well for this, although you could also use some global flag or one of the synchronization objects, which are covered later.

Thread Local Storage

In some cases, you use several different threads running the same thread function. For instance, you may be developing a DLL that handles connections to outside processes, and you want to create a new thread for each connection. Although you should still be careful to use the proper synchronization objects to protect global resources, the Win32 API also provides a mechanism to provide storage that is local to a thread.

This is generally necessary only if you are developing libraries that may be called by multiple threads that each require their own dynamic storage. For the threads created here, you could easily allocate memory within a thread and store the pointer in a variable local to the thread function of a worker thread or a member of your CWinThread class. Because threads have their own stack, variables local to the thread function are thread-local. Likewise, an instance of CWinThread has its own set of data members that are thread-local.

Allocating a TLS Index

To use thread-local storage (TLS), you must first allocate a TLS index with a call to TlsAlloc(). This function takes no parameters and will return a DWORD, which is the new TLS index. This index can then be used by any threads in the process in a call to TlsSetValue() or TlsGetValue() to access an LPVOID pointer that is local to that thread. When all threads are finished with a given TLS index, it may be deallocated with TlsFree().

Using Thread-Local Storage

This may all seem a bit confusing, so let's look at a more concrete example. Suppose you are creating a DLL that requires some of its own dynamic memory. Let's also allow the calling process to call the same functions with different threads to work with different connections.

When a process first attaches to the DLL, TlsAlloc() is called to allocate an index that you will use to store the connection data pointers for each thread. The index is stored somewhere where it can be accessed by the functions that need it (that is, globally).

When each thread attaches to the DLL, your DLL code will allocate a block of memory to hold data for this connection. You can then use the TLS index that you stored earlier in a call to TlsSetValue() to save a pointer to the new data.

Whenever a thread needs to access the connection data, it should first call TlsGetValue() with the TLS index you stored. This will return the pointer that was saved with TlsSetValue() for this thread. The pointer can then be used freely.

When a process detaches from the DLL, you should call TlsFree() to free the TLS index.

If you find that you require more than one pointer to thread-local storage, simply allocate a second index with TlsAlloc(). All systems are guaranteed to support at least 64 TLS indexes per process, although you can use the TLS_MINIMUM_AVAILABLE constant to see the actual minimum.

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 they all 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. However, creating a multithreaded application that works correctly is not a trivial task. Very careful consideration must be given 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, let's suppose that you have a linked list that uses pointers to dynamically allocated memory. Let's also suppose you have thread A that adds items to the list and thread B that deletes items from a list. If thread B is in the middle of deleting an item from the list when it is preempted, 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 can cause real problems. This sort of problem is, however, 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. Let's say one of your threads used 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 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() will lock the critical section and return, 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 will block until the critical section is available. This occurs when the first thread calls Unlock(), allowing other threads to access the critical section.

NOTE

The Lock() function will accept a timeout parameter, but it is simply ignored.

This provides a simple, lightweight mechanism to limit access to critical sections in your code; however, it does have its limitations.

First, use of the critical section 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 potentially be corrupted by simultaneous access by multiple threads. You will have to be responsible for calling Lock() before each bit of code that might cause problems. You will 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; they cannot be used to protect memory or other resources shared between processes. For this, you will need to use one of the beefier classes, such as CMutex.

CMutex

The MFC CMutex class is similar to CCriticalSection in that it can be used to provide mutually exclusive access to certain sections of your code. Like critical sections, CMutex objects will work to protect your data only if you use them, although they can be used 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 allows you to specify the ownership of the mutex when it is created. It is often 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, the security attributes for the mutex may be specified in the last parameter. Windows 95 simply ignores this parameter.

The Lock() and Unlock() functions are used in much the same way as the calls used for critical sections, except that Lock() allows you to specify a timeout value. If the mutex is unavailable when Lock() is called, the call will block 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() will return 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 will actually create the operating system mutex, as well as the CMutex object.

The next thread to call CMutex() will create a CMutex object corresponding to the mutex object that was already created with the same name. If the bInitallyOwn parameter is set to TRUE, the call to CMutex() will not return until the mutex becomes available. You can take advantage of this to create a CMutex only at the time 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, they may be created to allow access to only 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 will have to wait until a thread releases the semaphore, thereby incrementing 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 are used to 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 will 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 will 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, your program will occasionally want to wait for some event to occur, rather than waiting for a resource to become available. This is useful in 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, 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. These 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 allows you to set the initial value of the event. If this is FALSE, the event is initially 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 will behave. In general, if this parameter is TRUE, the event must be reset manually by your code after it is signaled. If the parameter is FALSE, the event object will reset itself. You will see how this works a bit more clearly when you see how to manipulate the event.

Like other CSyncObjects, you can wait on an event with the Lock() member, which allows 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 is not normally done. To signal an event, you can call SetEvent().

If bManualReset for the event is FALSE, this will allow only one thread that is waiting on the event to proceed (by returning TRUE from the thread's call to Lock()). The event is then reset before any other threads will return from Lock(). If no threads are waiting on the event when SetEvent() is called, the event will remain signaled until one thread is given a lock on it. The event is then set to unsignalled automatically.

If, on the other hand, bManualReset is TRUE, all threads that are waiting on the event will be allowed to continue (Lock() will return TRUE). The event is not reset automatically, so any subsequent calls to Lock() for the event will not have to wait, but will return TRUE. To manually reset the event, you can call ResetEvent(), thus making any further calls to Lock() for the event wait until it is signaled again.

In addition, the PulseEvent() member of CEvent may be used to signal an event. If bManualReset is TRUE, all threads that are currently waiting for the event will acquire a lock and the event is automatically reset.

If bManualReset is FALSE, one waiting thread will be able to acquire a lock before the event is reset. Note that if no threads are currently waiting on the event, the event is reset immediately. Unlike SetEvent(), PulseEvent() will not keep the event signalled until one thread acquires the lock.

CSingleLock

MFC provides classes that can 'simplify' access to the synchronization objects discussed previously. These include CMultipleLock, which allows threads to wait for combinations of objects (as you will see next), and CSingleLock, which can be used 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 will 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 will call Unlock() for you.

As you can probably 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 will often 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 will probably 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 can be used to specify a maximum time to wait for a lock (in milliseconds). The special value of INFINITE can be used to wait until you shut down the machine to install the latest greatest Win32 OS.

If the bWaitForAll parameter is TRUE, Lock() will not return successfully until all the objects handled in the CMultiLock are available. This is useful for situations where you need to lock several resources at one time, and it can go a long way toward preventing deadlocks. If this bWaitForAll is FALSE, Lock() will return whenever any one object is locked. This is handy for waiting for several different events, where you want to wait for any one to occur, but don't expect all of them.

In addition to the sync objects specified in the constructor for the CMultiLock, the Lock() function can use the dwWakeMask parameter to specify several Windows messages that will cause Lock() to return.

Although your thread will not receive any messages while it is blocked in a call to Lock(), MFC will check the messages that have been sent to your thread, to see whether the Lock() should return.

The flags that you may specify in dwWakeMask are listed in Table 9.1.

Table 9.1. dwWakeMask flags

Flag

Message type that will interrupt Lock()

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 forth

QS_MOUSEBUTTON

A mouse button message, such as WM_LBUTTONDOWN, WM_RBUTTONUP, and so forth

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

QS_ALLEVENTS

Any message, other than those covered by QS_SENDMESSAGE

Okay, so Lock() can return for 101 different reasons, but how do you know what caused the return?

CMultiLock::Lock() Return Values

To see what caused Lock() to stop waiting, you 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 will receive this return value, but not until all other objects specified in the CMultiLock are locked or abandoned.

Finally, if Lock() returns WAIT_TIMEOUT, you can probably guess that the timeout time has expired.

Cleaning Up

When you are finished with the sync objects that you have locked, you should call Unlock() to unlock all objects used by the CMultiLock. Although this will be called for you when the CMultiLock is destroyed, it is a good practice to call it explicitly, as soon as you are finished with the resources protected by sync objects.

Creating a New Process

In some very large applications, you may wish to create new processes, which include their own memory space and are independent of the process that created them. To do this, you can use CreateProcess().

In short, CreateProcess() takes a filename for an executable file that will be run in the new process. However, CreateProcess() also takes 27 other parameters, either directly or indirectly. Because this is seldom used by the majority of Win32 applications, you won't look at the details of CreateProcess() here. However, if you do need to do this, you will be relieved to know that most of the parameters to CreateProcess() have reasonable default values.

Summary

In this chapter, you have looked at a lot of the nuts and bolts that are used to construct a multithreaded application in MFC. It is now up to you, the developer, to ensure that your application uses these tools and your application uses threads properly, including proper thread synchronization.

You have seen how to set up your application to use threads, as well as how to create worker threads to do background processing without holding up other threads in your application.

You have also explored how to create user-interface threads that can manage windows independently, as well as the details of creating a class to be used to initialize your user-interface thread and handle messages that it receives.

In addition, you have learned how threads can use their own thread-local storage, as well as how to synchronize your threads to share common resources or use events to notify your threads when interesting things happen.


Document Info


Accesari: 3615
Apreciat: hand-up

Comenteaza documentul:

Nu esti inregistrat
Trebuie sa fii utilizator inregistrat pentru a putea comenta


Creaza cont nou

A fost util?

Daca documentul a fost util si crezi ca merita
sa adaugi un link catre el la tine in site


in pagina web a site-ului tau.




eCoduri.com - coduri postale, contabile, CAEN sau bancare

Politica de confidentialitate | Termenii si conditii de utilizare




Copyright © Contact (SCRIGROUP Int. 2024 )