Windows Message Processing

To understand threads, you must first understand how 32-bit Windows processes messages. The best starting point is a single-threaded program that shows the importance of the message translation and dispatch process. You'll improve that program by adding a second thread, which you'll control with a global variable and a simple message. Then you'll experiment with events and critical sections. For heavy-duty multithreading elements such as mutexes and semaphores, however, you'll need to refer to another book, such as Jeffrey Richter's Advanced Windows, 3d Ed. (Microsoft Press, 1997).

How a Single-Threaded Program Processes Messages

All the programs so far in this book have been single-threaded, which means that your code has only one path of execution. With ClassWizard's help, you've written handler functions for various Windows messages and you've written OnDraw code that is called in response to the WM_PAINT message. It might seem as though Windows magically calls your handler when the message floats in, but it doesn't work that way. Deep inside the MFC code (which is linked to your program) are instructions that look something like this:

MSG message;
while (::GetMessage(&message, NULL, 0, 0)) {
    ::TranslateMessage(&message);
    ::DispatchMessage(&message);
}

Windows determines which messages belong to your program, and the GetMessage function returns when a message needs to be processed. If no messages are posted, your program is suspended and other programs can run. When a message eventually arrives, your program "wakes up." The TranslateMessage function translates WM_KEYDOWN messages into WM_CHAR messages containing ASCII characters, and the DispatchMessage function passes control (via the window class) to the MFC message pump, which calls your function via the message map. When your handler is finished, it returns to the MFC code, which eventually causes DispatchMessage to return.

Yielding Control

What would happen if one of your handler functions was a pig and chewed up 10 seconds of CPU time? Back in the 16-bit days, that would have hung up the whole computer for the duration. Only cursor tracking and a few other interrupt-based tasks would have run. With Win32, multitasking got a whole lot better. Other applications can run because of preemptive multitasking—Windows simply interrupts your pig function when it needs to. However, even in Win32, your program would be locked out for 10 seconds. It couldn't process any messages because DispatchMessage doesn't return until the pig returns.

There is a way around this problem, however, which works with both Win16 and Win32. You simply train your pig function to be polite and yield control once in a while by inserting the following instructions inside the pig's main loop:

MSG message;
if (::PeekMessage(&message, NULL, 0, 0, PM_REMOVE)) {
    ::TranslateMessage(&message);
    ::DispatchMessage(&message);
}

The PeekMessage function works like GetMessage, except that it returns immediately even if no message has arrived for your program. In that case, the pig keeps on chewing. If there is a message, however, the pig pauses, the handler is called, and the pig starts up again after the handler exits.

Timers

A Windows timer is a useful programming element that sometimes makes multithreaded programming unnecessary. If you need to read a communication buffer, for example, you can set up a timer to retrieve the accumulated characters every 100 milliseconds. You can also use a timer to control animation because the timer is independent of CPU clock speed.

Timers are easy to use. You simply call the CWnd member function SetTimer with an interval parameter, and then you provide, with the help of ClassWizard, a message handler function for the resulting WM_TIMER messages. Once you start the timer with a specified interval in milliseconds, WM_TIMER messages will be sent continuously to your window until you call CWnd::KillTimer or until the timer's window is destroyed. If you want to, you can use multiple timers, each identified by an integer. Because Windows isn't a real-time operating system, the interval between timer events becomes imprecise if you specify an interval much less than 100 milliseconds.

Like any other Windows messages, timer messages can be blocked by other handler functions in your program. Fortunately, timer messages don't stack up. Windows won't put a timer message in the queue if a message for that timer is already present.

The EX12A Program

We're going to write a single-threaded program that contains a CPU-intensive computation loop. We want to let the program process messages after the user starts the computation; otherwise, the user couldn't cancel the job. Also, we'd like to display the percent-complete status by using a progress indicator control, as shown in Figure 12-1. The EX12A program allows message processing by yielding control in the compute loop. A timer handler updates the progress control based on compute parameters. The WM_TIMER messages could not be processed if the compute process didn't yield control.

Figure 12-1. The Compute dialog box.

Here are the steps for building the EX12A application:

  1. Run AppWizard to generate \vcpp32\ex12a\ex12a. Accept all the default settings but two: select Single Document and deselect Printing And Print Preview. The options and the default class names are shown here.

  2. Use the dialog editor to create the dialog resource IDD_COMPUTE. Use the resource shown here as a guide.

    Keep the default control ID for the Cancel button, but use IDC_START for the Start button. For the progress indicator, accept the default ID IDC_PROGRESS1.

  3. Use ClassWizard to create the CComputeDlg class. ClassWizard connects the new class to the IDD_COMPUTE resource you just created.

    After the class is generated, add a WM_TIMER message handler function. Also add BN_CLICKED message handlers for IDC_START and IDCANCEL. Accept the default names OnStart and OnCancel.

  4. Add three data members to the CComputeDlg class. Edit the file ComputeDlg.h. Add the following private data members:

    int m_nTimer;
    int m_nCount;
    enum { nMaxCount = 10000 };
    

    The m_nCount data member of class CComputeDlg is incremented during the compute process. It serves as a percent complete measurement when divided by the "constant" nMaxCount.

  5. Add initialization code to the CComputeDlg constructor in the ComputeDlg.cpp file. Add the following line to the constructor to ensure that the Cancel button will work if the compute process has not been started:

    m_nCount = 0;

    Be sure to add the line outside the //{{AFX_DATA_INIT comments generated by ClassWizard.

  6. Code the OnStart function in ComputeDlg.cpp. This code is executed when the user clicks the Start button. Add the following boldface code:

    void CComputeDlg::OnStart() 
    {
        MSG message;
    
        m_nTimer = SetTimer(1, 100, NULL); // 1/10 second
        ASSERT(m_nTimer != 0);
        GetDlgItem(IDC_START)->EnableWindow(FALSE);
        volatile int nTemp;
        for (m_nCount = 0; m_nCount < nMaxCount; m_nCount++) {
            for (nTemp = 0; nTemp < 10000; nTemp++) {
                // uses up CPU cycles
            }
            if (::PeekMessage(&message, NULL, 0, 0, PM_REMOVE)) {
                ::TranslateMessage(&message);
                ::DispatchMessage(&message);
            }
        }
        CDialog::OnOK();
    }
    

    The main for loop is controlled by the value of m_nCount. At the end of each pass through the outer loop, PeekMessage allows other messages, including WM_TIMER, to be processed. The EnableWindow(FALSE) call disables the Start button during the computation. If we didn't take this precaution, the OnStart function could be reentered.

  7. Code the OnTimer function in ComputeDlg.cpp. When the timer fires, the progress indicator's position is set according to the value of m_nCount. Add the following boldface code:

    void CComputeDlg::OnTimer(UINT nIDEvent)
    {
        CProgressCtrl* pBar = (CProgressCtrl*) GetDlgItem(IDC_PROGRESS1);
        pBar->SetPos(m_nCount * 100 / nMaxCount);
    }
    

  8. Update the OnCancel function in ComputeDlg.cpp. When the user clicks the Cancel button during computation, we don't destroy the dialog; instead, we set m_nCount to its maximum value, which causes OnStart to exit the dialog. If the computation hasn't started, it's okay to exit directly. Add the following boldface code:

    void CControlDlg::OnCancel()
    {
        TRACE("entering CComputeDlg::OnCancel\n");
        if (m_nCount == 0) {      // prior to Start button
            CDialog::OnCancel();
        }
        else {                    // computation in progress
            m_nCount = nMaxCount; // Force exit from OnStart
        }
    }
    

  9. Edit the CEx12aView class in ex12aView.cpp. First edit the virtual OnDraw function to display a message, as shown here:

    void CEx12aView::OnDraw(CDC* pDC)
    {
        pDC->TextOut(0, 0, "Press the left mouse button here.");
    }
    

    Then use ClassWizard to add the OnLButtonDown function to handle WM_LBUTTONDOWN messages, and add the following boldface code:

    void CEx12aView::OnLButtonDown(UINT nFlags, CPoint point)
    {
        CComputeDlg dlg;
        dlg.DoModal();
    }
    

    This code displays the modal dialog whenever the user presses the left mouse button while the mouse cursor is in the view window.

    While you're in ex12aView.cpp, add the following #include statement:

    #include "ComputeDlg.h"

  10. Build and run the application. Press the left mouse button while the mouse cursor is inside the view window to display the dialog. Click the Start button, and then click Cancel. The progress indicator should show the status of the computation.