Lesson 3: Using Multiple Threads

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:

  • 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.
Estimated lesson time: 60 minutes

Multithreaded Applications

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:

  • Scheduled (timer-driven) activities The thread that runs the automatic backup feature in the word processor example is blocked for five-minute intervals between backups. Thread schedules can be set with millisecond precision in Win32 applications.
  • Event-driven activities The threads can be triggered by signals from other threads. An example is a monitoring system, in which an error-logging thread is inactive until one of the other threads alerts it to an error condition.
  • Distributed activities When data must be collected from (or distributed to) several computers, it makes sense to create a thread for each request so that these tasks can proceed in parallel, in their own time frame.
  • Prioritized activities Win32 threads can be assigned priorities to determine the proportionate amount of execution time that is assigned to a thread by the thread scheduler. To improve a program's responsiveness, it is sometimes useful to divide its work into a high-priority thread for the user interface and a low-priority thread for background work.

Multithreading with MFC: CWinThread Class

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.

Worker Threads and User Interface Threads

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 Worker Threads

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.

Creating a Worker Thread

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.

  • To create the CTimer class
    1. Open the CMyApp project. Then in FileView, double-click the MainFrm.cpp file icon.
    2. To the top of the file, beneath the #include statements, add the following code:
    3.  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.

    4. Directly beneath the class declaration, add the following line to declare a global CTimer object:
    5. 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.

  • To add the DoTimer() function
    1. In the MainFrm.cpp file, directly beneath the class declaration, add the following function definition:
    2.  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.

    3. Your next step is to implement the dialog box that is used to set the timer time. Use the dialog editor to create a dialog box like the one shown in Figure 5.8.
    4. 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.

  • To set the IDC_DTPSETTIME control to display the time only
    1. In the dialog editor, select the IDC_DTPSETTIME control. Press ENTER to edit the control properties.
    2. Click the Styles tab. In the Format box, click Time.
    3. Click outside the property sheet to close it.

    You will now create a dialog class for the Timer dialog box.

  • To create the CTimerDialog class
    1. With the dialog editor open, press CTRL+W to open ClassWizard. When instructed, create the CTimerDialog class.
    2. Click the Member Variables tab. Create a Value member variable for the IDC_DTPSETTIME ID. This should be a variable of type CTime and be named m_settime.
    3. Click OK to close ClassWizard.

    Now all you have to do is add a command and handler function to set the timer time and start the timer thread.

  • To add the Timer option to the View menu and the OnViewTimer() handler function
    1. Use the menu editor to create the Timer command on the View menu. Accept the ID_VIEW_TIMER ID that is created by default.
    2. Use the Message Maps tab on ClassWizard to add a command handler for the ID_VIEW_TIMER object ID to the CMainFrame class. Name the function OnViewTimer().
    3. After the handler has been added, click the Edit Code button to locate the CMyAppApp::OnViewTimer() implementation. Add the following code to the body of the function:
    4.  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); } 

    5. To the top of the MainFrm.cpp file, along with the other #include statements, add the following line of code:
    6. #include "TimerDialog.h"

    7. Now compile and build the MyApp application. Test the timer operation by choosing the Timer command from the View menu. The Timer dialog box appears with the current time displayed in the Date Time Picker control. Use the control to set the timer time to a minute or two later than the current time. Click Start to close the Timer dialog box and start the timer.

    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.

    Creating User Interface Threads

    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.

  • To create the CMyUIThread class
    1. From within the CMyApp project, press CTRL+W to open ClassWizard.
    2. Click the Add Class button and choose New from the drop-down menu. The New Class dialog box appears.
    3. In the Name box, type CMyUIThread. Choose CWinThread from the Base class drop-down menu.
    4. Click OK to create the class, and then click OK to close ClassWizard.

    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.

    Thread Synchronization

    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

    NameDescription
    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:

    • Wrap global data objects or resource access functions inside a class. Protect the controlled data and regulate access through the use of public functions.
    • Within your class, create a synchronization object of the appropriate type. For example, you would use a CCriticalSection object to ensure that your global data is updated by only one thread at a time, or you might include a CEvent object to signify that a resource was ready to receive data.
    • In the member functions that give access to the data or resource, create an instance of a synchronization access object. Use CSingleLock when you need to wait on only one object at a time. Use CMultiLock when there are multiple objects that you could use at a particular time.
    • Before the function code attempts to access the protected data, call the Lock() member function of the synchronization access object. The Lock() function can be instructed to wait for a specified amount of time (or to wait indefinitely) for the associated synchronization object to become available. For example, a CCriticalSection object is available if it successfully manages to secure exclusive execution access for the current thread. The availability of an event is set in the program code by calling the CEvent::SetEvent() function.
    • After the function has finished accessing the protected data, call the access object's Unlock() function or allow the object to be destroyed.

    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.

  • To implement thread-safe access to the CTimer class data
    1. At the top of the MainFrm.cpp file, with the other #include statements, add the following line:
    2. #include <afxmt.h>

    3. Add this line of code to the protected section of the CTimer class definition:
    4. CCriticalSection m_CS;

    5. Remove the inline definitions of the CTimer::GetTime() and CTimer::SetTime() functions (remember to add semicolons in their place).
    6. Beneath the CTimer class definition, add the CTimer::GetTime() function definition as follows:
    7. CTime CTimer::GetTime() {      CSingleLock csl(&m_CS);      csl.Lock();      CTime time = m_time;      csl.Unlock();      return time; }

    8. Add the following CTimer::SetTime() function definition beneath the lines of code you added in step 4:
    9. 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.

    Lesson Summary

    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.



    Microsoft Press - Desktop Applications with Microsoft Visual C++ 6. 0. MCSD Training Kit
    Desktop Applications with Microsoft Visual C++ 6.0 MCSD Training Kit
    ISBN: 0735607958
    EAN: 2147483647
    Year: 1999
    Pages: 95

    flylib.com © 2008-2017.
    If you may any questions please contact us: flylib@qtcs.net