With its multi-tasking and multi-threading API, Win32 revolutionized programming for Microsoft Windows. If you've seen magazine articles and advanced programming books on these subjects, you might have been intimidated by the complexity of using multiple threads. You can stick with single-threaded programming for a long time and still write useful Win32 applications. But if you learn the fundamentals of threads, you'll be able to write more efficient and capable programs. You'll also be on your way to a better understanding of the Win32 programming model.
Windows Message Processing
In order 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 can improve that program by adding a second thread, which you'll control with a global variable and a simple message. Then you can experiment with events and critical sections. For heavy-duty multi-threading elements such as mutexes and semaphores, however, you'll need to refer to another book, such as Jeffrey Richter's Programming Applications for Microsoft Windows, Fourth Edition (Microsoft Press, 1999).
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 Microsoft Visual Studio'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 if 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 key messages into character messages. For example, WM_KEYDOWN messages are translated into WM_CHAR messages containing ASCII characters. 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 were 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, multi-tasking got a whole lot better. Other applications can run because of preemptive multi-tasking— Windows simply interrupts your pig function when it needs to. However, even in Win32, your program will be locked out for 10 seconds. It won't be able to process any messages because DispatchMessage won't return until the pig returns.There is a way around this problem, however, which works with both Win16 and Win32. You simply yield control once in a while by inserting the following instructions inside the main loop:
MSG message;
if (::PeekMessage(&message, NULL, 0, 0, PM_REMOVE)) {
::TranslateMessage(&message);
::DispatchMessage(&message);
}
The PeekMessage function works like GetMessage, except it returns immediately even if no message has arrived for your program. In that case, the messages keep flowing. If there is a message, however, the function pauses, the handler is called, and the message processing starts up again after the handler exits.
Timers
A Windows timer is a useful programming element that sometimes makes multi-threaded 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 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 Ex11a 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 won't be able to cancel the job. Also, we'd like to display the percent-complete status by using a progress indicator control, as shown in Figure 11-1. The Ex11a 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 11-1: The Compute dialog box.
Here are the steps for building the Ex11a application:
Run the MFC Application Wizard to generate a project named Ex11a. Choose New Project from Visual Studio's File menu. In the New Project dialog box, select the MFC Application template, type the name Ex11a, and click OK. In the MFC Application Wizard, accept all the default settings but two: On the Application Type page, select Single Document, and on the Advanced Features page, deselect Printing And Print Preview.
Create a new dialog resource named IDD_COMPUTE. Choose Add Resource from the Project menu and add a new dialog resource. Change the ID property for the dialog box to IDD_COMPUTE and change the Caption property to Compute. For the OK button, change the ID property to IDC_START and change the Caption property to Start. For the Cancel button, change the ID property to IDC_CANCEL. Using the Toolbox, add a Progress control and leave the default ID property as IDC_PROGRESS1. When you're finished, your dialog box should look like the following:

Use the MFC Class Wizard to create the CComputeDlg class. Choose Add Class from the Project menu to display the MFC Class Wizard. Type CComputeDlg as the class name, select CDialog as the base class, and set the dialog ID to IDD_COMPUTE to connect the new class to the dialog resource you just created.
Add WM_TIMER and BN_CLICKED message handlers. Select the CComputeDlg class in Class View. Click the Messages button at the top of the Properties window and add the OnTimer function for the WM_TIMER message. Click the Events button at the top of the Properties window and add the OnBnClickedStart and OnBnClickedCancel functions for IDC_START and IDC_CANCEL.
Add three data members to the CComputeDlg class. Edit the file

int m_nTimer;
int m_nCount;
enum { nMaxCount = 50000 };
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.
Add initialization code to the CComputeDlg constructor in the

m_nCount = 0;
Code the OnBnClickedStart function in

void CComputeDlg::OnBnClickedStart()
{
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);
}
}
GetDlgItem(IDC_START)->EnableWindow(TRUE);
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 OnBnClickedStart function could be reentered. The second call to EnableWindow(TRUE) enables the Start button so the user can run the timer again.
Code the OnTimer function in

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

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

void CEx11aView::OnDraw(CDC* pDC)
{
CEx11aDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
pDC->TextOut(0, 0, "Press the left mouse button here.");
}
Then add the OnLButtonDown member function. Select the CEx11aView class in Class View. In the Properties window, click the Messages button, select the WM_LBUTTONDOWN message, and add the OnLButtonDown function. Add the following boldface code:
void Cex11aView::OnLButtonDown(UINT nFlags, CPoint point)
{
CComputeDlg dlg;
dlg.DoModal();
CView::OnLButtonDown(nFlags, point);
}
This code displays the modal dialog box whenever the user presses the left mouse button while the mouse cursor is in the view window.While you're in

#include "ComputeDlg.h"
Build and run the application. Press the left mouse button while the mouse cursor is inside the view window to display the dialog box. Click the Start button, and then click Cancel. The application should terminate the computation and fill the rest of the progress control.