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(CSieveApp)
    public:
    virtual BOOL InitInstance();
    //}}AFX_VIRTUAL

// Implementation

    //{{AFX_MSG(CSieveApp)
       // NOTE - the ClassWizard will add and remove member functions here.
       //    DO NOT EDIT what you see in these blocks of generated code !
    //}}AFX_MSG
    DECLARE_MESSAGE_MAP()
};

///

//`AFX_INSERT_LOCATION`
// 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_MAP(CSieveApp)
        // NOTE - the ClassWizard will add and remove mapping macros here.
        //    DO NOT EDIT what you see in these blocks of generated code!
    //}}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(CSieveDlg)
    protected:
    virtual void DoDataExchange(CDataExchange* pDX);    // DDX/DDV support
    //}}AFX_VIRTUAL

// Implementation
protected:
    HICON m_hIcon;

    // Generated message map functions
    //{{AFX_MSG(CSieveDlg)
    virtual BOOL OnInitDialog();
    afx_msg void OnPaint();
    afx_msg HCURSOR OnQueryDragIcon();
    afx_msg void OnStart();
    //}}AFX_MSG
    afx_msg LONG OnThreadFinished (WPARAM wParam, LPARAM lParam);
    DECLARE_MESSAGE_MAP()
};

//`AFX_INSERT_LOCATION`
// 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(CSieveDlg)
        // NOTE: the ClassWizard will add member initialization here
    //}}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(CSieveDlg)
        // NOTE: the ClassWizard will add DDX and DDV calls here
    //}}AFX_DATA_MAP
}

BEGIN_MESSAGE_MAP(CSieveDlg, CDialog)
    //{{AFX_MSG_MAP(CSieveDlg)
    ON_BN_CLICKED(IDC_START, OnStart)
    //}}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;
}

Calling MFC Member Functions Across Thread Boundaries