Threads--Concurrency and Synchronization
What is a thread? It’s a piece of executable code which is given its own time slice by the operating system.
A processowns at least one thread, and also owns memory space. A thread shares the memory space of the process that owns it. Every thread is owned by some process.
Usually, your program’s main thread manages the user interface, and any other threads manage computational tasks.
The class CWinThread wraps the Win32 thread object. As usual with MFC wrapper classes, in addition to creating the C++ object, you must call the thread’s Create function to create the associated Win32 thread. Sometimes one hears "application thread" and "system thread". These are other terms for CWinThread objects and Win32 threads.
Every thread has a function (known as a “thread procedure”) which specifies its task. There are two kinds of threads: interface threads, and worker threads. In an interface thread, the thread procedure has a message loop, so it processes Windows messages, while the thread procedure of a worker thread does not. You cannot send a Windows message to a worker thread.
When we speak of a “message loop”, we mean something like the code we discussed at the beginning of the semester. Roughly,
while(msg = GetMessage())
{ DispatchMessage(msg);
}
Possible States of a Thread
Ready. Waiting for the CPU to give it a time slice so it can execute code.
Running. Using the CPU right now.
Blocked. Waiting for input.
Terminated. Has finished, by executing a return or exit statement, or throwing an unhandled exception.
Scheduler
This is a component of the operating system that switches the states of threads.
Interthread Communication
Since all threads owned by a process have access to the memory owned by that process, they can communicate by writing to and reading from that memory. They can treat it as a “bulletin board”.
One can also communicate with an interface thread by sending it a Windows message. This can be done using the SendMessage or PostMessage functions, which in MFC appear as member functions in the CWnd class. You cannot communicate with a worker thread in this way, since its Run does not have a message loop.
Kruglinksi's Example Program
1. Make an SDI application, accepting all defaults. I called mine ThreadTest.
2. Insert a new dialog, with ID number IDD_COMPUTE, change the OK button to Start (ID_START), and add a progress bar:
The point of the example: when you press Start, some long computation will begin in a worker thread, and its progress will be shown on the progress bar, and you can interrupt it with the Cancel button at any time.
3. Use Class Wizard to add a class CComputeDlg corresponding to this dialog. Add to this class, button handlers OnStart and OnCancel.
4. Add global variables (integers) g_nCount and g_nMaxCount,
and initialize them in the constructor of CComputeDlg to 0 and 10000, respectively. Add a member variable m_nTimer of type int.
5. Add OnLButtonDown to the view class, and make it bring up the dialog:
void CThreadTestView::OnLButtonDown(UINT nFlags, CPoint point)
{
CComputeDlg dlg;
dlg.DoModal();
}
Of course you will have to add #include "ComputeDlg.h" in your view class .cpp file before you can compile this. If you like you can add
pDC->TextOut(5,5,"Press the left mouse button here.") in OnDraw. Now the dialog will come up when you click the mouse.
6. Map the WM_TIMER message in CComputeDlg:
void CComputeDlg::OnTimer(UINT nIDEvent)
{ CProgressCtrl *pBar =
( CProgressCtrl *)GetDlgItem(IDC_PROGRESS1);
pBar->SetPos(g_nCount *100/ g_nMaxCount);
}
The variable g_nCount is going to measure the progress. The position needs to be set in percentages, so it has to be scaled. The thread is going to run until g_nCount == g_nMaxCount.
- What do we want the worker thread to do? We just make up something that will take up time. Just let the computer count to ten thousand, ten thousand times. After each count to ten thousand, it increments g_nCount. When it finishes, it posts a message to the main thread. That message is a user-defined message (explained below). The following code will be executed by the worker thread. In this approach, this code is a global function, not belonging to any class. You can put it in the CompDlg.cpp file, for example.
UINT ComputeThreadProc(LPVOID pParam)
{ volatile int nTemp;
for(g_nCount = 0; g_nCount < g_nMaxCount ;
::InterlockedIncrement((long*) &g_nCount))
{ for(nTemp = 0; nTemp < 10000; nTemp++)
{ ; // use up CPU cycles
}
}
::PostMessage((HWND) pParam,
WM_THREADFINISHED, 0,0);
g_nCount = 0;
return 0;
}
There are some points to explain about this code.
- What is InterlockedIncrement and why do we use it?
- What is volatile and why do we use it?
- Why PostMessage instead of SendMessage?
- How do we define and handle a user-defined message?
- How is pParam going to be equal to the view window's handle?
- Why use the view window's handle instead of a pointer to the view window as a CWnd object?
We will take these questions one at a time.
- What is InterlockedIncrement and why do we use it?
The worker threads should be terminated from within--there is no
terminate member of the CWinThread class. When the master thread wants to terminate the worker thread, it does so indirectly, by setting g_nCount to a value greater than g_nMaxCount. That should cause the worker thread's main loop to complete and the thread to terminate naturally.
Suppose we just use g_nCount++ to increment g_nCount. What actually happens in the assembly code generated by g_nCount++ is that the value of g_nCount is loaded into a register, then the register is incremented, then the contents of the register are stored in g_nCount.
Suppose the worker thread is interrupted after the value, say 41, is loaded into the register. The main thread wants to terminate the worker, and therefore it changes the value of g_nCount to a value greater than g_nMaxCount. The worker thread gets control again and stores the incremented value, 42, back in g_nMaxCount. The attempt to terminate the thread has failed. This bug would not be reproducible and would not happen a very high percentage of the time, but if this code ran on a web server, with one thread per client, it would happen often enough to make plenty of trouble.
InterlockedIncrement is there just to solve this problem.
- What is volatile and why do we use it?
The key word volatile tells the compiler that this value is subject to modification by other threads or processes, and therefore should not be stored in a register, and code using it should not be optimized, but left as written. In this case, its purpose is to prevent a modern optimizing compiler from deleting the do-nothing loop of 10000 steps. It has nothing to do with thread safety in this example. Since optimizations are not applied in the debug version of your MFC program, if you did not use volatile, you wouldn't notice anything until/unless you compiled the release version.
- Why PostMessage instead of SendMessage?
PostMessage will put the message in the application message queue, and return immediately. The application's message pump will call the handler when the message comes to the head of the queue.
SendMessage will call the message handler immediately, and not return until the message handler is finished. That is, IF the message handler is in the same thread. But in this case, it is in a different thread. There are several complicated pages in Jeff Richter's book Advanced Windows Programming about how SendMessage works when sending a message to a different thread. The most important point is that the thread will not be interrupted and forced to process the message immediately, if it is doing something else. Instead, the sending thread will be forced to wait until the other thread is ready to process messages. For that reason PostMessage is better, unless it makes no sense to go on until the message has been processed.
Also, even within the same thread, you shouldn't call message handlers while a modal dialog is up--messages should wait until the dialog has been terminated. Besides, there's no reason for the worker thread to give up control now. It should go ahead and terminate. The message is supposed to tell the master that the worker thread HAS TERMINATED, not "is about to terminate".
- How do we define and handle a user-defined message?
We put the line
#define WM_THREADFINISHED (WM_USER+5)
in some header file that is included in all the files that mention the new message. For example, in this program, I put it in ThreadTest.h. The identifier WM_USER marks the place where the Win32 API says you can start defining your own messages. But MFC uses a few, so you are advised to start with WM_USER+5 when writing MFC programs.
Don't put this line in resource.h, since Visual Studio is always regenerating that file, and will wipe out your new line.
Now to get the message mapped to the right message handler. You cannot do this with Class Wizard, which cannot handle user-defined messages. Here are the steps:
In ComputeDlg.cpp, after BEGIN_MESSAGE_MAP but outside the AFX_MSG_MAP brackets:
ON_MESSAGE(WM_THREADFINISHED,
CComputeDlg::OnThreadFinished)
It goes all on one line, though it won't fit on one line here.
In ComputeDlg.h, add the prototype of the handler:
afx_msg LRESULT OnThreadFinished(WPARAM wParam, LPARAM lParam);
Then, in ComputeDlg.cpp, add the message handler itself.
LRESULT CComputeDlg::OnThreadFinished(WPARAM wParam, LPARAM lParam)
{ GetDlgItem(ID_START)->EnableWindow(TRUE);
KillTimer(m_nTimer); // otherwise there’s a memory leak
return 0;
}
This leaves the dialog up; we removed the OK button. Of course, we could call CDialog::OnOK() to dismiss it, but instead we just leave it up, enabling the Start button again.
Here it is in context: Italics show the added code.
In ComputeDlg.cpp:
BEGIN_MESSAGE_MAP(CComputeDlg, CDialog)
ON_MESSAGE(WM_THREADFINISHED,
CComputeDlg::OnThreadFinished)
//{{AFX_MSG_MAP(CComputeDlg)
ON_BN_CLICKED(ID_START, OnStart)
ON_WM_TIMER()
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
In ComputeDlg.h:
class CComputeDlg : public CDialog
{
// Construction
public:
int m_nCount;
CComputeDlg(CWnd* pParent = NULL);
afx_msg LRESULT OnThreadFinished(WPARAM wParam, LPARAM lParam);
// Dialog Data
//{{AFX_DATA(CComputeDlg)
That finishes the explanation of user-defined messages.
- Why use the view window's handle instead of a pointer to the view window as a CWnd object?
Because the classes derived from CObject are not "thread-safe".
The details of exactly what this means are complicated, and as a normal Windows programmer, you do not need to know them. You just need to know that you should not pass pointers to objects derived from CWnd from one thread to another. It is all right to pass a pointer to CRect or CString, for example, but not to a CWnd, or for example to a CButton, since CButton is derived from CWnd.
7. Now we've programmed the worker thread. Here's how to stop it:
void CComputeDlg::OnCancel()
{ if(g_nCount == 0) // prior to Start button
CDialog::OnCancel();
else // computation in progress
g_nCount = g_nMaxCount; // force exit
}
8. It only remains to start the thread. Here's the way Kruglinksi does it. Below I discuss another way, which is often better.
void CComputeDlg::OnStart()
{
m_nTimer = SetTimer(1,100,NULL);
GetDlgItem(ID_START)->EnableWindow(FALSE);
AfxBeginThread(ComputeThreadProc, GetSafeHwnd(), THREAD_PRIORITY_NORMAL);
}
Well, the program works. You can press Start, and then interrupt the thread by pressing Cancel. You can tell it worked, because the progress bar stops and the Start button is re-enabled. Or, if you don't interrupt, the thread stops when it is done, and the Start button is re-enabled.
Creating Worker Threads
Method 1 (as in the Kruglinksi example)
Supply a pointer to a global “thread procedure” that will be used in place of Run. This pointer is used as a parameter to the MFC function Afx_BeginThread. Although this works OK in the example given in this lecture, it is harder to work with in more realistic examples.
Method 2
The default Run in CWinThread has a message loop, so to create a worker thread, you first create a class derived from CWinThread, and then override Run.