Multithreading: The Construction of Abortable, Lengthy Operations

Introduction

This article shows how to use multithreading to construct abortable lengthy operations, in an MFC environment.

The design and implementation techniques, especially with respect to thread communication and synchronization, can be used in many other classes of multithreaded applications as well.

The multithreading technique is commonly used to avoid blocking a program (for example, GUI responsiveness) when some “lengthy operation,” like the calculation of an algorithm, is to be performed. It is good user interface design to provide the user with an option to abort such an operation. For a lenghty operation to be “abortable” with an immediate response, it must be virtually interruptable at any time. Although not being trivial (such as long-lasting SQL SELECT database queries often cannot be broken into smaller units in an obvious way), this is assumed here.

The Differences Between Worker Threads and User-Interface Threads

When creating a new thread in an MFC application, you generally have the choice of using either a so-called “worker thread” or a “user-interface thread.” Because there is a lot of confusion about these terms, I will describe the differences between the two and explain why I choose to put the “lengthy operation” not in a worker thread, but in a user-interface thread.

Worker Threads

A worker thread is created most easily by a call to, for example, AfxBeginThread, with a pointer to the thread function as a parameter:


//create and run worker thread
//2nd parameter is a pointer to argument
AfxBeginThread(&threadFunc,pargs);

//thread function
unsigned int threadFunc( void* pArgument )
{
//perform the thread code,
//
//…
//
//and terminate yourself, simply by leaving the function
return 0;
}

The thread function may contain arbitrary code, such as a “lengthy operation.” The thread terminates itself after having reached the thread function’s closing brackets. Often, the thread function will consist of a non-blocking endless loop, suspending itself by a call to one of the Win32 synchronization functions, such as WaitForSingleObject.

The endless loop has to be programmed explicitly. This is one of the disadvantages when compared to a user interface thread.

In an MFC environment, it is often necessary for a separate thread to communicate with MFC objects in some way. Therefore, it is sometimes suggested to pass, for example, a pointer to a CWnd instance as a parameter to the thread function. By doing so, this can give you all kinds of trouble in your application. Although possible in principle, it is much more difficult and error-prone to access MFC objects from a worker thread than from a user-interface thread[1].

User-Interface Threads

The term “user-interface thread” (UI thread) is misleading. It suggests that the thread has an user interface. This does not, by any means, need to be the case. A UI thread does not need (as in our example) to have a user interface. It has two primary advantages:


  • It has a built-in message loop, and therefore relieves the programmer from coding it explicitly.

  • It communicates with other MFC objects without a problem.

For these reasons, UI threads as secondary threads in an MFC environment are a good choice, often without any user interface. “Without any” is meant here literally, especially with respect to message boxes, to avoid losing messages[2].

In non-MFC, multithreaded environments, you have to use worker threads. NT services, always being multithreaded, are such a class of applications.

A UI thread needs an instance of a CWinThread-derived class to work. As a consequence, it has (among others) the overridable method InitInstance. The message loop is started by returning TRUE from InitInstance. Messages posted to it by PostThreadMessage are mapped to message handlers by the ON_THREAD_MESSAGE macro. Be careful not to use any message boxes (or other windows) in your secondary UI thread when waiting for messages posted by another thread by PostThreadMessage. They can get lost!


//create and run user-interface thread
//m_pUIThread is a pointer to an instance of CUiThread,
//derived from CWinThread
//The CUiThread class is usually created by class wizard.
m_pUiThread =
(CUiThread*)AfxBeginThread( RUNTIME_CLASS(CUiThread));

Architecture of the Application

Any multithreaded application has to be designed carefully with respect to thread synchronization and communication. Therefore, it is a good idea to make some sort of interaction diagram, to be sure to avoid side-effects like “race-conditions.”

The primary thread with its CMainFrame class starts the lengthy operation when the user clicks the menu item “Lengthy Operation,” the menu handler being OnLenop().

The menu handler creates and starts the UI thread (instance of class CUiThread). The UI thread waits to start the lengthy operation until it gets its start trigger, the user-defined message WU_UITHREAD_START. Before that message is posted by the OnLenop() menu handler, the non-modal abort dialog (an instance of class CDgAbort) is created. After that, the WU_UITHREAD_START message is posted by PostThreadMessage.

Being at this point, we reached the top of the shaded part of the CUiThread bar in our interaction diagram (time flowing from top to bottom). All is arranged now in an orderly way, the abort dialog is created and ready, and the UI thread will start its lengthy operation.

The lengthy operation now runs (in the context of the UI thread) until completion, or until it is aborted by a click on the Abort button of the abort dialog. The global variable g_abort is set to true; for the UI thread, this is the indication to abort the lengthy operation.

The UI thread signals this by posting the user-defined message WU_LENOP_TERMINATED to the main frame class, using PostMessage, not PostThreadMessage, because the mainframe has a window receiving messages. The return code of the lengthy operation is sent as a parameter of that message.

The mainframe destroys the abort dialog, and signals the UI thread to terminate itself, using the message WU_LENOP_TERMINATED. When the mainframe is sure that the UI thread has been terminated (by calling WaitForSingleObject, not shown in the diagram), it signals a message box to the user indicating either the success or the abort of the operation. The corresponding information is communicated by the UI thread as a termination code (parameter of PostQuitMessage).

To achieve high responsiveness, both threads should have the same priority. This automatically is the case if the priority is not changed explicitly by the program, for example, by modifying the corresponding default parameter value in AfxBeginThread.

The application can be divided in three phases: start phase, running phase, and terminating phase. These phases are described in the following sections.

Start Phase

The OnLenop() menu handler looks like this:


//start lengthy operation
void CMainFrame::OnLenop()
{
if (m_pUiThread) //avoid to start lenop more than once
return;

// Prepare the user interface thread
m_pargs = new ty_args;
m_pargs->hWnd = GetSafeHwnd();
m_pargs->inputVal = m_pargs->returnVal = 0;

m_pUiThread =
(CUiThread*)AfxBeginThread( RUNTIME_CLASS(CUiThread));

m_pDgAbort = new CDgAbort;
m_pDgAbort->Create(IDD_ABORT, this);
m_pargs->inputVal = 1234567;

//post a user defined message to the “lenghty op” user
//interface thread (having no user interface at all)
m_pUiThread->PostThreadMessage(WU_UITHREAD_START,
0,
(LPARAM)m_pargs);
}

First of all, the menu handler checks whether the secondary thread is already running, by simply checking if the (private) member variable is not equal to 0.

The ty_args datatype is a simple structure with parameters passed to the UI thread. It consists of an input value, a return value, and a handle to the mainframe window. The window handle instructs the UI thread the destination where to PostMessage. It simply looks like this:


typedef struct
{
HWND hWnd;
int inputVal;
int returnVal;
}ty_args;

By posting WU_UITHREAD_START, the secondary thread is instructed to start the lengthy operation.

The start phase of the UI thread simply returns TRUE in InitInstance, starting the message loop.


BOOL CUiThread::InitInstance()
{
return TRUE; //start message pump
}

The abort dialog has no specific start phase.

Running Phase

In the UI thread, the running phase begins with the start message WU_UITHREAD_START from the main thread. The message is mapped to the handler OnStart:


//perform the lengthy operation
void CUiThread::OnStart(WPARAM, LPARAM lp)
{
ty_args* pargs = (ty_args*)lp;
int i = pargs->inputVal;
int k = lengthyOperation (i);
//signal the initiating window the termination of the lenop,
//together with some params
//with a user defined message
CWnd::FromHandle(pargs->hWnd)->PostMessage(WU_LENOP_TERMINATED, i, k);
}

Note the signature of the OnStart handler, having a “void” return code. This is significantly different from handlers for messages posted to windows with PostMessage; their menu handler must have the LRESULT return code. Not observing this results in strange errors like programs running in Debug Mode but not in Release Mode!

For our example, the lengthy operation is trivial. It is a for-loop, checking the global g_abort flag in every round.


//do something lasting some time
//this may equally well be a member function of CUiThread
int lengthyOperation (const int maxCnt)
{
g_abort = false;
for (int i=0; i < maxCnt; i++)
{
for (int j=0; j < maxCnt/10000; j++)
{
double di = i*i*i*i; //do something
if (g_abort) //check abort flag
return i;
}
}
return 0;
}

After the Running Phase, the UI thread idles until it is requested to terminate itself, again by a user-defined message of the primary thread (mainframe).

The Running Phase of the main thread is functionally split in two tasks: on the one hand the normal application, running concurrently with the secondary UI thread (lengthy operation), and the abort dialog, presenting to the user the possibility to abort the lengthy operation.

Note that in the main thread there is concurrency between the mainframe and other windows of the MFC framework (like views), and the abort dialog. This kind of concurrency is not achieved by using multithreading, but by the design of the window’s message loop, distributing messages to all windows, assuming that the message handlers terminate in rather short time.

The Running Phase of the abort dialog is nearly trivial. It just waits for a user to click the “Abort” button. In this case, the global abort flag is set, and the dialog terminates itself. The self-posting of the WM_CLOSE message closes the window, but does not destroy the dialog. This will be done in the termination phase by the main thread.

Although the abort dialog has no close button, it is possible to close it by other means, for example, by pressing ALT+F4. To prevent this, a private flag (m_closeRequest) is set to indicate the handler of WM_CLOSE the origin of the close request. The dialog is closed only when this flag is set.


//global flag for the aborted state
extern bool g_abort;
void CDgAbort::OnAbort()
{
//signal the abort request to the UI thread
g_abort = true;
//close yourself
m_closeRequest = true;
PostMessage(WM_CLOSE);
}

void CDgAbort::OnClose()
{
if (m_closeRequest) //make sure the close request comes
//from clicking Abort
CDialog::OnClose();
}

Termination Phase

In the termination phase, the main thread is responsible for deleting the secondary thread and the abort dialog, and processing the result of the lengthy operation. In our case, this “processing” is a simple message box to the user.

The termination phase is initiated in the main thread’s message handler (called OnLenopTerm) of the WU_LENOP_TERMINATED message posted by the UI thread to the mainframe window.


LRESULT CMainFrame::OnLenopTerm(WPARAM wp, LPARAM lp)
{
int input = (int)wp;
int result= (int)lp;

//destroy the abort dialog
//do NOT delete m_pDgAbort: self-deletion of
// nonmodal dialog in PostNcDestroy.
if (m_pDgAbort)
m_pDgAbort->DestroyWindow();
m_pDgAbort = 0;
delete m_pargs;
m_pargs = 0;

if (m_pUiThread) //sanity check
{
HANDLE hThread = m_pUiThread->m_hThread;
HANDLE hThreadDuplicated = 0;
DWORD threadExitCode = (DWORD)-1;
DWORD error = (DWORD)-1;

//get a duplicated handle of the thread, to be able to
//retrieve its exit code
BOOL ret = DuplicateHandle(GetCurrentProcess(),
hThread,
GetCurrentProcess(),
&hThreadDuplicated,
0,
FALSE,
DUPLICATE_SAME_ACCESS);
if (!ret)
error = GetLastError();

//signal to the ui thread to terminate itself
m_pUiThread->PostThreadMessage(WU_LENOP_TERMINATED,0,0);

//make sure the thread terminated itself before continuing
//to avoid “race conditions”
WaitForSingleObject(hThread, INFINITE);

//get thread exit code, in case it is needed
//if we do not use the duplicated handle, we get an
//”Handle invalid” error
//Another possibilty to get the exit code is to set the
//thread’s m_bAutoDelete to FALSE, and delete the thread
//manually.
ret = GetExitCodeThread(hThreadDuplicated, &threadExitCode);
if (!ret)
error = GetLastError();

CloseHandle(hThreadDuplicated);
CString s, s2;
if (threadExitCode == 123) //terminated by aborting
s2 = “Thread aborted with exit code: %d\r\nResult:\r\n_
Input: %d, Output: %d”;
else
s2 = “Thread exited normally with exit code: %d\r\nResult:\r\n_
Input: %d, Output: %d”;
s.Format(s2, threadExitCode, input, result);
AfxMessageBox (s);
}
m_pUiThread = 0;
return 0;
}

First, the abort dialog is destroyed and the parameter structure is deleted.

To retrieve the return code of the UI thread (such as the parameter of the UI thread’s PostQuitMessage call), it is not sufficient to call GetExitCodeThread with the thread handle. The handle must be duplicated before the thread terminates, by calling DuplicateHandle.

Next, the thread is signalled to terminate itself, by posting WU_LENOP_TERMINATED to it. To be sure that the thread really terminated before continuing, the programmer waits for the UI thread’s handle to enter the “signalled state,” by a call to WaitForSingleObject.

After that, the exit code can be retrieved by a call to GetExitCodeThread with the duplicated handle as a parameter.

The Termination Phase of the secondary UI thread is quite simple: The message handler of the WU_LENOP_TERMINATED message received from the mainframe just calls PostQuitMessage, with a different parameter for normal or aborted termination of the lengthy operation:


void CUiThread::OnTerminate(WPARAM, LPARAM)
{
//Terminate this ui thread with a return code indicating
//whether terminated normally, or by user abort.
//The return code is evaluated by the mainframe.
//You may check that exit code in DevStudio’s Output window.
if (g_abort)
PostQuitMessage(123);
else
PostQuitMessage(456);
}

Terminating the Application

The mainframe should get a handler of the WM_CLOSE message to deal with the situation of a running secondary UI thread when a user wants to terminate the application. In this case, a message box is displayed, indicating to abort the lengthy operation first.


void CMainFrame::OnClose()
{
if (m_pUiThread)
{
AfxMessageBox(“First abort the lengthy operation.”);
return;
}
CMDIFrameWnd::OnClose();
}

References

[1] MSDN article “Multithreading, Programming Tips,” and there “Accessing MFC objects from Non-MFC Threads.”

[2] MS article Q183116: PRB: PostThreadMessage Messages Lost When Posted to UI Thread

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read