Multithreading using MFC in Plain English: Part I

Part I of this tutorial covers basic concept of multithreading. If you have never used multithreading in your application and wish to know how to use multithreading in your applications and why, Then this article might be useful for you. I will cover samples and advanced topics of multithreading in the Part II of this tutorial.

Threading Concepts:

A thread is a path of execution. Each application has a default thread called 'Main Thread' or 'Primary
Thread
'. You can create multiple threads in your program. Other threads besides primary threads are called
'Secondary Threads'. Each process has at least one of more threads.

Multithreading:

Running more than one thread simultaneously is multithreading.

Why multiple threads?

Sometimes your program requirement is to do more than one task simultaneously. For example, if you have a program which reads a large database, generate some reports and print them. Suppose generating and printing these reports take few hours. If you don't use multithreading, you can't do anything if you are generating reports. Now what if you want to feed some input data when its generating time consuming reports? In present scenario you can't. But your program supports multithreading, one thread can takes care of reports and you can use other thread to feed input data.

Did you use FrontPage or MS Word? This is one good example of multithreading. When you type wrong spelling of any word, it let you know by drawing an underline. It does spell checking in a background thread and you even notice it. Neat? huh?

MFC and Multithreading:

MFC supports two types of threading: User Interface Threads ( UI Threads ) and Worker Threads. UI threads have a GUI with a message pump so user can trap the messages such as mouse or keyboard. Worker thread doesn't have message pump and used to execute background talks such as big calculations, or generating reports.

Before we create our secondary threads, lets see the definition of one important function.

afxBeginThread: - This function creates a new thread by calling CWinThread's CreateThread function and
returns a CWinThread object. Here is syntax:

CWinThread* AfxBeginThread( AFX_THREADPROC pfnThreadProc, LPVOID pParam, int nPriority = THREAD_PRIORITY_NORMAL, UINT nStackSize = 0, DWORD dwCreateFlags = 0, LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL );

CWinThread* AfxBeginThread( CRuntimeClass* pThreadClass, int nPriority = THREAD_PRIORITY_NORMAL, UINT nStackSize = 0, DWORD dwCreateFlags = 0, LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL );

Parameter Description
pfnThreadProc controlling function, cannot be NULL. This function must be declared as follows: UINT MyControllingFunction( LPVOID pParam );
pThreadClass The RUNTIME_CLASS of an object derived from CWinThread.
pParam Parameter to be passed to the controlling function as shown in the parameter to the function declaration in pfnThreadProc.
nPriority Thread priority. See below table.
nStackSize Specifies the size in bytes of the stack for the new thread. If 0, the stack size defaults to the same size stack as the creating thread.
dwCreateFlags additional flag and can be any of these two CREATE_SUSPENDED or 0. CREATE_SUSPENDED starts the thread with a suspend count of one. The thread will not execute until ResumeThread is called. 0 Start the thread immediately after creation.
lpSecurityAttrs Points to a SECURITY_ATTRIBUTES structure. See SECURITY_ATTRIBUTES for more details.

Thread Priorities:

Priority Meaning
THREAD_PRIORITY_ABOVE_NORMAL Indicates 1 point above normal priority for the priority class.
THREAD_PRIORITY_BELOW_NORMAL Indicates 1 point below normal priority for the priority class.
THREAD_PRIORITY_HIGHEST Indicates 2 points above normal priority for the priority class.
THREAD_PRIORITY_IDLE Indicates a base priority level of 1 for IDLE_PRIORITY_CLASS, BELOW_NORMAL_PRIORITY_CLASS, NORMAL_PRIORITY_CLASS, ABOVE_NORMAL_PRIORITY_CLASS, or HIGH_PRIORITY_CLASS processes, and a base priority level of 16 for REALTIME_PRIORITY_CLASS processes.
THREAD_PRIORITY_LOWEST Indicates 2 points below normal priority for the priority class.
THREAD_PRIORITY_NORMAL Indicates normal priority for the priority class.
THREAD_PRIORITY_TIME_CRITICAL Indicates a base priority level of 15 for IDLE_PRIORITY_CLASS, BELOW_NORMAL_PRIORITY_CLASS, NORMAL_PRIORITY_CLASS, ABOVE_NORMAL_PRIORITY_CLASS, or HIGH_PRIORITY_CLASS processes, and a base priority level of 31 for REALTIME_PRIORITY_CLASS processes.

Creating User Interface Thread:

A UI thread has a GUI with a message pump and commonly used to handle user input. The main thread in your application is a UI thread. Here are few steps to create a UI thread:

1. Derive a class from CWinThread and override its functions

Function name Purpose
InitInstance Perform thread instance initialization. Must be overridden.
Run Controlling function for the thread. Contains the message pump. Rarely overridden.
OnIdle Perform thread-specific idle-time processing. Not usually overridden.
PreTranslateMessage Filter messages before they are dispatched to TranslateMessage and DispatchMessage. Not usually overridden.
ProcessWndProcException Intercept unhandled exceptions thrown by the thread's message and command handlers. Not usually overridden.
ExitInstance Perform cleanup when thread terminates. Usually overridden.

2. Create a thread by calling afxBeginThread with RUNTIME_CLASS of the class you derived from CWinThread as first parameter of afxBeginThread function. Rest parameters are optional. See afxBeginThread for more details.

Creating a Worker Thread:

A worker thread is commonly used to handle backgrounds tasks that the you shouldn't have to wait to continue using your application. Creating a secondary thread is pretty easy task. There are only two steps:

Step 1. Create a function which will be executed by secondary thread. This function has your code which suppose to executed in secondary thread.

UINT ProcName(LPVOID param)
{
{
Do Something
}
return 0;
}

Step 2. Create thread by calling MFC function AfxBeginThread.

AfxBeginThread( ProcName, param, priority );

This thread remains active as long as thread's function is executing. When thread function exits, the thread is
destroyed. You can set thread priorities according to your requirement.

Sample Example:

Step 1. Create an SDI application with all default settings accept turn off all check boxes of Step 4 of 6 of
AppWizard.

Step 2. Add a menu item 'Thread' with a sub menu item 'Start Thread'. Write corresponding command handler
using ClassWizard.

Step 3. Type this code below and here is how your function look like:

void CMainFrame::OnThreadStart()
{
// CODE_START
HWND hWnd = GetSafeHwnd();
AfxBeginThread( mcProc, hWnd, THREAD_PRIORITY_NORMAL );
// CODE_END
}

Step 4. Create a global function. Type this code just before this command handler function OnThreadStart

UINT mcProc(LPVOID param)
{
::MessageBox( (HWND)param, "Thread Start", "Secondary Thread", MB_OK );
return 0;
}

Step 5. Build, and execute your project and click Start Thread menu item.

Thread Synchronization:

If more than one thread uses same resources ( variables such as a link list ) then it is very important to provide a synchronization between them. Ok let's say, two threads A and B are using a list. When thread A is reading data, thread B is writing data. To avoid data inconsistency thread synchronization is must.

Win 32 supports 4 types of thread synchronization objects:

  •  Events
  • Critical Sections
  • Mutexes
  • Semaphores

Events:

Events are used to provide synchronization between two or more concurrently running threads. An event is a BOOL type variable. This can have two states, either set or reset. If two threads are using same list, and say ThreadA is writing data to the list, then ThreadA will reset that event. Now if ThreadB wants to access that data, it will first see if that event is set or not. If it is not set then it will wait. ThreadA will set this event as it is done writing data to the list. One this event is set, ThreadB has the acccess.

MFC's CEVent class encapsulate this functionality. It provides two members SetEvent and ResetEvent.

Critical Sections:

A critical section is an area of the code which is locked by one thread and this thread doesn't release this area until it is done it job. During CS, only one thread can access that code. Let's say, ThreadA is writing some data to a data structure, it will lock that area of code until it is done writing and it will only unlock after it is done.

MFC's CCriticalSection class encapsulate this functionality. It provides members functions Lock and UnLock. It's easy to use CS in your applications:

CCriticalSection cs;
.....
....
// ThreadA writes data to an array
cs.Lock();
// Write data to an array
cs.Unlock();
....
// ThreadB reads data to an array
cs.Lock();
// Read data from an array
cs.Unlock();

Mutexes:

Critical Sections can only work for single process while mutexes can be used for mutiple process using multiple
threads. You can use mutexes for multiple threads in same process too. Let's say, you have one txt file. You
are running two processes ProcA and ProcB. You want to make sure that ProcB can't have access to the txt file
if ProA is reading/writing it.

CMutex mx( FALSE, "tada");
.....
....
// ProcA writes data to the file
mx.Lock();
// Writes data
mx.Unlock();
....
CMutex mx( FALSE, "tada");
// ProcB writes data to the file
mx.Lock();
// Writes data
mx.Unlock();

We will see how to use in our examples.

Semaphores:

Semaphores can be used to synchronize threads of a single process or multiple processes. The use of semaphores depends of number of resources available. CSemaphore class of MFC also have Lock and Unlock functions but it works in different way. Initially you have to allocate available resources. Locking a semaphore decrements a resource count and unlocking it increases the resource count. When resource count is 0, that means there are no resources available. Rest of the threads have to wait until another threads free the resources.

CSemaphore cph( 1, 5) ; // This means initial resource count 1 with max 5 resources
....
cph.Lock(); // decrement count
// access the resource
cph.Unlock(); // increment count

We will see how to use in our examples.

Thread Termination and other operations:

There are different ways to terminate a thread.

1. Normal Thread Termination :

For a worker thread, when a thread exits its controlling function, call either AfxEndThread function or a return 0. For a user-interface thread, ::PostQuitMessage. The  ::PostQuitMessage takes is the exit code of the thread. Call GetExitCodeThread  to get the exit code of a thread.  Return 0 is for successful completion.

2. Premature Thread Termination:

AfxEndThread is used to terminate a thread from within the thread. Pass the desired exit code as the only parameter. This stops execution of the thread, deallocates the thread's stack, detaches all DLLs attached to the thread, and deletes the thread object from memory.

Suspending and Resuming a Thread:

You can use CWinThread's ResumeThread and SuspendThead to resume and suspend a thread.

Thread Sleep:

You can put a thread to sleep by using Sleep Win32 API.

::Sleep(1000);

Debugging Multithreading Applications:

Debugging a multithreaded application is not easy task. The best thing to to avoid bugs. There are two most
common problems in multithreading applications. One is deadlock and other is access violation.

The deadlock situation occurs when multiple threads try to lock same resource at the same time. For example,
say your application has two threads Thread1 and Thread2. Thread1 locks resource ResA and Thread2 locks
ResB. Now Thread1 tries to lock ResB but wait to unlock ResA until it locks RestB which is already locked by
Thread2. And Thread2 tries to lock ResA and wait to unlock ResB until it locks ResA.

Other problem in multithreaded applications are access violation. The main reason of access violation is when
more than one thread try to access the same memory simultaneously or when shared memory has been released
by other thread. Or other reason may be when one thread try to access a variable which is released by another
thread.

Now point comes how to debug this kind of applications? And the main problem is when you try to debug this
kind of applications, you won't get access violation in debug mode. The reason might be that debugger has
stopped the other thread.

First thing to avoid this situation is try to synchronize your code. Use thread synchronization methods to access
shared memory and don't let this situation arrive. What if you can't avoid this situation?? Well .. You can try
one of two ways. I use ASSERT and message boxes when application is not big. But if application is big then
creating log file(s) is not a bad approach. You write your values to these log files with your custom comments.
You can have a separate log file for each thread and some common log files.

Designing a Thread Safe Class:

Why thread safe class? A thread safe class is a class which doesn't need any synchronization mechanism when
it is used in more than one thread or applications. The members of this class can be shared by multiple threads
but there won't be any data inconsistency because your class provides thread safety internally.

Creating a thread safe class is pretty easy. Just wrap all of its member functions in Lock and Unlock. Hopefully
you will see one example in my next part of this tutorial.

CAUTIONS:

Caution 1: MFC classes are not thread safe

MFC classes are not thread safe on object level. That means if you use same object in two threads, data may
not be safe. For example, if you use a CFile object in two classes, you must use some synchronization scheme
to make sure your data is safe or use two separate objects.

Caution 2: MFC Objects from Win32 Threads

If you want to use MFC objects in your secondary thread, you must create that thread using MFC ( above UI or
Worker thread techniques ). If you have created a thread using _beginthread Win32 API then you can't use MFC
objects in that thread.

posted on 2006-11-02 22:40  cy163  阅读(...)  评论(...编辑  收藏

导航