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 multitaskingWindows 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:
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.
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.
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.
m_nCount = 0;
Be sure to add the line outside the //{{AFX_DATA_INIT comments generated by ClassWizard.
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.
void CComputeDlg::OnTimer(UINT nIDEvent) { CProgressCtrl* pBar = (CProgressCtrl*) GetDlgItem(IDC_PROGRESS1); pBar->SetPos(m_nCount * 100 / nMaxCount); }
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 } }
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"