In Lesson 2 of Chapter 4, you learned that a process is an instance of an executing program. A thread is a path of execution within a process and is the smallest piece of code that can be scheduled to run on the processor. A thread runs in the address space of the process and uses resources allocated to the process. All processes have at least one thread of execution, known as the primary thread. You can create additional secondary threads to take advantage of the multitasking capabilities of Windows 32-bit operating systems.
In this lesson, you will learn how to enhance the capabilities and the performance of your application by creating secondary threads of execution using MFC classes and global functions.
After this lesson, you will be able to:Estimated lesson time: 60 minutes
- Describe when to use multithreaded programming techniques.
- Describe the two different types of thread that you can implement using MFC.
- Describe the role of the MFC class CWinThread in the implementation of secondary threads.
- Describe how to create and terminate secondary threads in an MFC application.
- Describe how to use the MFC synchronization classes to control thread access to shared data and resources.
You can use multiple threads in your application wherever an improvement in performance can be gained by running separate tasks concurrently. As an example, consider a word processing application that automatically backs up the current document every five minutes. The user's input to the document, via the main application window, is handled by the primary thread. The application code can create a separate secondary thread that is responsible for scheduling and performing the automatic backups. Creating a secondary thread prevents the backing up of lengthy documents from interfering with the responsiveness of the application's user interface.
Situations where using multiple threads can deliver performance benefits to your application include:
All threads in MFC applications are represented by CWinThread objects. This includes the primary thread of your application, which is implemented as a class derived from CWinApp. CWinApp is directly derived from CWinThread.
Although the Win32 API provides the _beginthreadex function, which allows you to launch threads at a low level, you should always use the CWinThread class to create threads that use MFC functionality. This is because the CWinThread class uses thread-local storage to manage information specific to the context of the thread in the MFC environment. You can declare CWinThread objects directly, but in many cases, you will allow the global MFC function AfxBeginThread() to create a CWinThread object for you.
The CWinThread::CreateThread() function is used to launch the new thread. The CWinThread class also provides the functions SuspendThread() and ResumeThread() to allow you to suspend and resume the execution of a thread.
MFC distinguishes between two types of threads: worker threads and user interface threads. This distinction is made solely by MFC; the Win32 API does not distinguish between threads.
Worker threads are commonly used to complete background tasks that do not require user input. Examples could include database backup functions or functions that monitor the current state of a network connection.
User interface threads are able to handle user input, and they implement a message loop to respond to events and messages generated by user interaction with the application. The best example of a user interface thread is the application primary thread represented by your application's CWinApp-derived class. Secondary user interface threads can be used to provide a means to interact with an application without degrading the performance of other application features. For example, consider an application that allows anesthetists to monitor the condition of a patient undergoing surgery. A user interface thread could be used to allow the anesthetists to enter details of the drugs that they have administered without interrupting the threads that handle the monitoring of a patient's vital signs.
You create secondary threads in an MFC application by calling the global function AfxBeginThread(). There are two overloaded versions of AfxBeginThread(), one for creating worker threads, and one for creating user interface threads. The following sections demonstrate how these two versions are used.
Creating a worker thread is simply a matter of implementing a controlling function that performs the task your thread is to perform, and passing the address of the controlling function to the appropriate version of the AfxBeginThread() function.
The controlling function should have the following syntax:
UINT MyControllingFunction(LPVOID pParam); |
The parameter is a single 32-bit value. The parameter can be used in a number of ways, or it can be ignored. It can pass a simple value to the function or a pointer to a structure containing multiple parameters. If the parameter refers to a structure, the structure can be used not only to pass data from the caller to the thread, but also to pass data back from the thread to the caller.
The worker-thread version of AfxBeginThread() is declared as follows:
CWinThread* AfxBeginThread(AFX_THREADPROC pfnThreadProc, LPVOID pParam, int nPriority = THREAD_PRIORITY_NORMAL, UINT nStackSize = 0, DWORD dwCreateFlags = 0, LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL); |
The first two parameters are the address of the controlling function and the address of the parameter that is to be passed to it. The remaining parameters (which all have default values) allow you to specify the thread priority, its stack size, whether it is to be created in a suspended state, and whether it is to run immediately. The final parameter allows you to specify the security attributes for the thread—the default value NULL means that the thread will inherit the attributes of the calling thread.
AfxBeginThread() creates a new CWinThread object, calls its CreateThread() function to start executing the thread, and returns a pointer to the thread. Checks are made throughout the procedure to make sure all objects are deallocated properly should any part of the creation fail. To end the thread, you call the global function AfxEndThread() from within the thread or simply return from the controlling function of the worker thread. The return value of the controlling function is commonly used to indicate the reason for termination. Traditionally, the exit code is 0 if the function was successful. Nonzero values are used to indicate specific types of errors.
The following exercises illustrate how to create a worker thread for the MyApp application. You will create a simple timer function that displays a message box when the system time reaches the timer setting. The user will set the timer using a dialog box. After the timer has been set, a worker thread will monitor the system clock once every second. After the timer time has been reached, the thread will display the message box and terminate.
You will create the CTimer class to encapsulate the timer. This class will contain a protected MFC CTime variable to store the timer time. It will also contain a public CWinThread pointer, which will be used to determine whether the CTimer object is currently referenced by an active thread.
class Ctimer { protected: CTime m_time; public: CWinThread * m_thread; CTimer() {Reset();} CTime GetTime() {return m_time;} void SetTime(CTime time) {m_time = time;} void Reset() { m_time = CTime::GetCurrentTime(); m_thread = NULL; } }; |
The GetCurrentTime() function is a static member function of the CTime class that returns the current system time in CTime format.
CTimer g_timer; |
You will now implement a controlling function for the worker thread. This function will compare the system time to the timer time at one-second intervals. After the timer time is reached, the function will display a message box and reset the timer.
UINT DoTimer(LPVOID pparam) { CTime currenttime = CTime::GetCurrentTime(); while(currenttime < g_timer.GetTime()) { Sleep(1000); currenttime = CTime::GetCurrentTime(); } AfxMessageBox("Time's up!"); g_timer.Reset(); return 0; } |
The Sleep() function causes the thread on which the function is called to be suspended for the specified number of milliseconds.
Figure 5.8 The Timer dialog box
The dialog box as a whole should have the ID IDD_TIMER. The control that looks similar to a combo box is a Date Time Picker control, which is available on the control toolbar. This control should be given the ID IDC_DTPSETTIME and should be set to display the time only.
You will now create a dialog class for the Timer dialog box.
Now all you have to do is add a command and handler function to set the timer time and start the timer thread.
CTimerDialog aTDlg; aTDlg.m_time = g_timer.GetTime(); if(aTDlg.DoModal() == IDOK) { g_timer.SetTime(aTDlg.m_time); // Only one timer running per instance if(!g_timer.m_thread) g_timer.m_thread = AfxBeginThread(DoTimer, 0); } |
#include "TimerDialog.h" |
While waiting for the timer, experiment with the other features of the MyApp interface. You will notice that their performance is not affected by the independent worker thread checking the system clock every second.
When the system clock catches up with the time set in the Timer dialog box, the message box appears and the worker thread terminates.
As we stated earlier, all threads in an MFC application are represented by objects of the CWinThread class, which are created by the AfxBeginThread() function. When you create a worker thread, AfxBeginThread() creates a generic CWinThread object for you, and assigns the address of your controller function to the CWinThread::m_pfnThreadProc member variable. To create a user interface thread, you must derive your own class from CWinThread and pass run-time information about the class to the user interface version of AfxBeginThread().
The following code snippet, taken from the MFC source code, shows how the framework distinguishes between a worker thread and a user interface thread. The code is taken from _AfxThreadEntry(), the entry-point function for all MFC threads.
// First — check for simple worker thread DWORD nResult = 0; if (pThread->m_pfnThreadProc != NULL) { nResult = (*pThread->m_pfnThreadProc)(pThread->m_pThreadParams); ASSERT_VALID(pThread); } // Else — check for thread with message loop else if (!pThread->InitInstance()) { ASSERT_VALID(pThread); nResult = pThread->ExitInstance(); } else { // Will stop after PostQuitMessage called ASSERT_VALID(pThread); nResult = pThread->Run(); } // Clean up and shut down the thread threadWnd.Detach(); AfxEndThread(nResult); |
If m_pfnThreadProc points to a controller function, the code knows that it is dealing with a worker thread. The controller function is called and the thread is terminated. If m_pfnThreadProc is NULL, the function assumes that it is dealing with a user interface thread. The thread object's InitInstance() function is called to perform thread initialization—to create the main window and other user interface objects, for example. If InitInstance() returns successfully (i.e., by returning TRUE), the Run() function is called. CWinThread::Run() implements a message loop to process messages directed to the thread's main window.
The InitInstance() and the Run() functions should sound familiar to you. They are two of the key virtual functions that are inherited by CWinApp, which was described in Lesson 3 of Chapter 3, in connection with the MFC implementation of the Win32 application architecture.
You are obliged to provide an overloaded version of InitInstance() for your thread class. Very often you will provide class-specific cleanup functions in an overloaded version of CWinThread::ExitInstance(). Generally, you will use the base class version of the Run() function.
The following exercise demonstrates the easiest way to create a CWinThread-derived class.
Open the MyUIThread.h and the MyUIThread.cpp files to inspect the source code for your thread class. Note that ClassWizard has provided stub implementations of the InitInstance() and ExitInstance() functions, to which you should add your class-specific implementation code. Note, too, that a message map is implemented for your thread class. The DECLARE_DYNCREATE and IMPLEMENT_DYNCREATE macros play an important role here, as they implement the run-time class information for CObject-derived classes that is required by the user interface thread version of AfxBeginThread().
After you have completed the implementation of your thread class, you call the user interface thread version of AfxBeginThread(), passing your thread class's run-time class information as a pointer as follows:
AfxBeginThread(RUNTIME_CLASS(CMyUIThread)); |
The RUNTIME_CLASS macro returns a pointer to the CRuntimeClass structure that maintains run-time type information for all CObject-derived classes declared with the DECLARE_DYNAMIC, DECLARE_DYNCREATE, or DECLARE_SERIAL macros. This enables AfxBeginThread() to create a thread object of the correct type.
The user interface thread version of AfxBeginThread() has the same set of default parameters as the worker thread version. It also returns a pointer to the thread object that it creates.
Secondary threads are generally used to implement asynchronous processing. An asynchronous operation is one that executes independently of other events or actions. Consider our timer thread, which runs along its own path of execution, checking the system clock once every second. It does not need to wait upon events in the application primary thread, and the application can proceed without waiting for the thread to finish its task.
There are a number of situations where these asynchronous activities will need to synchronize, or coordinate their operations. As an example, consider a print scheduler thread that queues print job threads created by different applications. The print job threads will need to notify the scheduler that they want to join the queue, and the scheduler will send a message to each print job in the queue when its turn comes to print.
Another scenario that typically requires thread synchronization is the updating of global application data. Consider a monitoring application with a sensor thread that updates a data structure with readings from a sensor device. Another thread is used to read the structure and display a representation of its state on the screen. Now suppose that the display thread tries to obtain a reading at the very millisecond that the data structure is being updated by the sensor thread. The result is likely to be corrupted data, which could have serious consequences if the application was, for example, monitoring a nuclear power plant. Thread access to global data needs to be synchronized to ensure that only one thread can read or modify a thread at any time.
One way to implement thread synchronization is to use a global object to act as an intermediary between threads. MFC provides a number of synchronization classes, listed in Table 5.4. These synchronization classes, derived from the base class CSyncObject, can be used to coordinate asynchronous events of any kind.
Table 5.4 MFC Synchronization Classes
Name | Description |
---|---|
CCriticalSection | A synchronization class that allows only one thread from within the current process to access an object. |
CMutex | A synchronization class that allows only one thread from any process to access an object. |
CSemaphore | A synchronization class that allows between one and a specified maximum number of simultaneous accesses to an object. |
CEvent | A synchronization class that notifies an application when an event has occurred. |
The synchronization classes are used in conjunction with the synchronization access classes CSingleLock and CMultiLock to provide thread-safe access to global data or shared resources. The recommended way to use these classes is as follows:
Encapsulating global resources and synchronization code inside a thread-safe class helps you centralize and control access to the resource and protect against a deadlock situation. A deadlock is a condition in which two or more threads wait for one another to release a shared resource before resuming their execution. Deadlock conditions are notoriously difficult to reproduce or to track down, so you are strongly advised to analyze your multithreaded application for possible deadlock conditions and take steps to prevent their occurrence.
The following exercise illustrates the procedure for providing thread-safe access to global data. You will use a CCriticalSection object and a CSingleLock object to protect access to the CTime object contained within the CTimer class that you created in the previous exercise.
#include <afxmt.h> |
CCriticalSection m_CS; |
CTime CTimer::GetTime() { CSingleLock csl(&m_CS); csl.Lock(); CTime time = m_time; csl.Unlock(); return time; } |
void CTimer::SetTime(CTime time) { CSingleLock csl(&m_CS); csl.Lock(); m_time = time; csl.Unlock(); } |
The calls to CSingleLock::Unlock() in these functions are not strictly necessary—we just consider them good programming style.
You can now build and run the application. You will not notice any difference in the operation of the program, but you can rest assured that the data encapsulated in any instance of the CTimer class can safely be accessed by any number of threads.
Multiple threads can be used in an application wherever an improvement in performance might be gained by running separate tasks concurrently. Asynchronous operations that run in their own time frame, operations scheduled by a timer, and operations that need to wait on events triggered by other threads are all possible candidates for implementation as secondary threads in an application process.
All threads that use MFC functionality should be created by an instance of the MFC CWinThread class. MFC provides the global function AfxBeginThread() to assist you in the process of creating threads in an MFC application. MFC distinguishes between two types of threads. Worker threads are commonly used to complete background tasks that do not require user input. User interface threads are able to handle user input, and they implement a message loop to respond to events and messages generated by the user interaction with the application.
To create a worker thread, you implement a controlling function that performs the task that your thread is to perform, and then pass the function address to the worker-thread version of AfxBeginThread(). To create a user interface thread, you derive your own class from CWinThread, provide overloads for key member functions, and pass run-time information about the class to the user interface thread version of the AfxBeginThread() function.
Secondary threads are generally used to implement asynchronous processing. However, there are a number of situations where threads will need to synchronize their activity. They might need to wait on a signal from another thread, or send signals to other threads. Thread access to global data needs to be synchronized to ensure that simultaneous access attempts do not result in data corruption.
MFC provides a number of synchronization classes derived from the base class CSyncObject, which can be used to coordinate asynchronous thread activity. These classes are used in conjunction with the synchronization access objects CSingleLock and CMultiLock.
When controlling thread access to a shared resource, the recommended practice is to encapsulate the resource inside a class, and to implement synchronization objects as protected members of the same class. Use the synchronization access objects within the class member functions to ensure that the resources are accessed in only a thread-safe manner—so that not more than one thread is allowed at any given time.