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.
Calling MFC Member Functions Across Thread Boundaries
转载于:https://blog.51cto.com/fengshuiyue/371854