Introduction
When we create a worker thread, our intention is to perform a task in the separate thread, i.e. a worker thread, without blocking the main thread. As a matter of fact, it is reasonably fair to say, what we want to be done is simply to call a function asynchronously.
Unfortunately, when we need to create a new thread for this simple task, there are some issues due to the limits of the relevant APIs as well as other issues which we must be concerned about. Some of these issues are simple and trivial, while others are subtle, and it is often really hard to identify the source of the problem at all, which it is the nature of multithreading.
But if what we've been doing for so many years fits into the same script, a boilerplate code snippet can be repeated over and over again. We have learned how to use ::CreateThread()
, _beginthread[ex]()
, and AfxBeginThread()
to spawn a new worker thread, and how to pack and unpack a set of information to pass over to the worker thread through a void
pointer.
Let's assume that we have a lengthy synchronous target function which is required to be executed in a separate thread 'asynchronously'.
class CMyWindow : public CWnd
{
int MyLengthyFunction(long param1, std::string tag)
{
...
}
};
In order to execute the synchronous function in a separate thread, we will need to use one of the thread creation APIs.
class CMyWindow : public CWnd
{
int MyLengthyFunction(long param1, std::string tag)
{
...
}
struct pack_parameter
{
CMyWindow * pthis;
long param1;
std::string tag;
};
bool CreateThreadAndCallMyLengthyFunction(long param1, std::string tag)
{
std::auto_ptr<pack_parameter> param_ptr(new pack_parameter);
param_ptr->pthis = this;
param_ptr->param1 = param1;
param_ptr->tag = tag;
CWinThread * pThread = AfxBeginThread(
&CMyWindow::MyThreadProc, param_ptr.get() );
ASSERT( pThread );
if( pThread )
{
param_ptr.release();
return true;
}
return false;
}
static UINT MyThreadProc(LPVOID parameter)
{
std::auto_ptr<pack_parameter> param_ptr(
static_cast<pack_parameter *>( parameter ) );
try
{
int result = param_ptr->pthis->MyLengthyFunction(
param_ptr->param1, param_ptr->tag );
}
catch(...)
{
ASSERT( false );
}
return 0;
}
void test()
{
CreateThreadAndCallMyLengthyFunction( 123L, "job#1" );
}
};
The above example shows one of the frequent thread creating scenarios to perform a lengthy task in a separate worker thread. Since the thread procedure accepts one and only one void
parameter as its input parameter, all input parameters for the lengthy function must be packed and unpacked on heap memory to be passed over to the thread procedure.
And, there are many other issues left which require our further attention such as return value handling, exception handling, thread synchronization, and so on. While an experienced and knowledgeable programmer can implement and handle all these subtle issues in the correct and efficient way, there is a great chance for a novice to overlook some of these issues ignorantly, in turn creating bugs that are really hard to debug.
We've seen that several thread libraries are out there which simply wraps those thread relevant APIs, but I will say that those libraries are half matured and incomplete, provided that creating a worker thread is all about calling a function asynchronously. They are no better than calling raw thread APIs, in my opinion.
Using the afc library, it becomes extremely easy to call a function asynchronously in a separate thread without blocking the main thread. Forget about the ancient myth saying that a thread procedure should be specified as a non member function. See below.
#include "afc.hpp"
class CMyWindow : public CWnd
{
int MyLengthyFunction(long param1, std::string tag)
{
...
}
void test()
{
afc::launch<afc::mfc_thread<> >(
&CMyWindow::MyLengthyFunction, this, 123L, "job#1" );
}
};
afc::launch<>()
is a function template helper to create an afc
proxy object (i.e., afc::detail::afc_proxy_t<>
) which delegates for both the worker thread spawned and the target function that is being executed in the thread.
Using the Code
A. Thread Traits
When using the afc::launch<>()
helper function template, a thread trait should be explicitly specified as the template parameter. There are three thread traits provided for the afc library; afc::win32_thread
, afc::crt_thread
, and afc::mfc_thread
, which correspond to ::CreateThread()
, _beginthreadex()
, and AfxBeginThread()
, respectively. If any of the CRT functions need to be called in the target function, which is executed in a separate worker thread, you must use either crt_thread
or mfc_thread
. In the same context, if any of the MFC functions need to be called, you are only allowed to use mfc_thread
; otherwise, the initialization of the necessary data structures which need to be switched on a per-thread basis will be skipped, which means your target function might not work as you would expect.
These thread traits themselves are a class template, and three thread parameters can be specified as non-type template parameters to customize the thread.
namespace afc
{
template<LPSECURITY_ATTRIBUTES ThreadAttributes = NULL
, int Priority = THREAD_PRIORITY_NORMAL, size_t StackSize = 0>
struct win32_thread;
template<LPVOID security = NULL
, int Priority = THREAD_PRIORITY_NORMAL, size_t StackSize = 0>
struct crt_thread;
template<LPSECURITY_ATTRIBUTES ThreadAttributes = NULL
, int Priority = THREAD_PRIORITY_NORMAL, size_t StackSize = 0>
struct mfc_thread;
}
It is not possible to automatically determine what type of thread trait is required, thus the proper thread trait should be specified manually whenever afc::launch<>()
is called.
#include "afc.hpp"
int my_function_use_win32_only(long param1, std::string tag);
int my_function_may_use_crt(long param1, std::string tag);
int my_function_may_use_mfc(long param1, std::string tag);
class CMyWindow : public CWnd
{
void test()
{
afc::launch<win32_thread<> >( &my_function_use_win32_only, 123L, "job#1" );
afc::launch<crt_thread<> > ( &my_function_use_win32_only, 234L, "job#2" );
afc::launch<mfc_thread<> > ( &my_function_use_win32_only, 345L, "job#3" );
afc::launch<crt_thread<> > ( &my_function_may_use_crt, 567L, "job#5" );
afc::launch<mfc_thread<> > ( &my_function_may_use_crt, 678L, "job#6" );
afc::launch<mfc_thread<> > ( &my_function_may_use_mfc, 012L, "job#9" );
}
};
B. Thread Completion Routine
afc::launch<>()
does not block the caller thread and return immediately; however, it does not mean that the spawned worker thread has been completed. If you want to retrieve the return of the target function call which is executed in the separate thread, or be notified of the event of completion of the target function, use the afc::on_completion<R>()
function template where R
is the return type of the target function call, but often omitted through the automatic template argument deduction.
#include "afc.hpp"
class CMyWindow : public CWnd
{
int MyLengthyFunction(long param1, std::string tag)
{
...
return 333;
}
void OnMyLengthyFunctionComplete(int ret, UINT error_code,
ULONG_PTR completion_key)
{
ASSERT( 333 == ret );
ASSERT( 777 == completion_key );
...
}
void test()
{
afc::launch<afc::mfc_thread<> >(
&CMyWindow::MyLengthyFunction, this, 123L, "job#1",
afc::on_completion( &CMyWindow::OnMyLengthyFunctionComplete, this, 777 ) );
}
};
Like Boost.Function
or Boost.Bind
, a special provision is made so that the first input argument specified right after the member function pointer is treated as either the pointer on which the member function call is made or a smart pointer object which provides the get_pointer()
overload. Internally, afc uses Boost.Bind
to pack all input arguments for the target function call.
void test()
{
afc::launch<afc::mfc_thread<> >(
&CMyWindow::MyLengthyFunction, this, 123L, "job#1",
afc::on_completion( &CMyWindow::OnMyLengthyFunctionComplete, this, 777 ) );
afc::launch<afc::mfc_thread<> >(
boost::bind( &CMyWindow::MyLengthyFunction, this, 123L, "job#1" ),
afc::on_completion( &CMyWindow::OnMyLengthyFunctionComplete, this, 777 ) );
}
When the execution of the MyLengthyFunction()
function call is completed in the separate worker thread, OnMyLengthyFunctionComplete()
will be invoked with the first parameter specified as the return of the MyLengthyFunction()
function call.
The function signature for the completion routine is predefined, and the completion routine whose function signature matches with the predefined one should be provided through the afc::on_completeion<>()
function template.
- Predefined function call signature of the completion routine for the target function of non-void return:
void (R, UINT, ULONG_PTR)
Predefined function call signature of the completion routine for the target function of void return.
void (UINT, ULONG_PTR)
#include "afc.hpp"
void OnMyLengthyFunctionComplete1(int ret, UINT error_code,
ULONG_PTR completion_key)
{
ASSERT( 999 == ret );
ASSERT( 111 == completion_key );
...
}
void OnMyLengthyFunctionComplete2(UINT error_code,
ULONG_PTR completion_key)
{
ASSERT( 222 == completion_key );
...
}
class CMyWindow : public CWnd
{
int MyLengthyFunction1(long param1, std::string tag)
{
...
return 999;
}
void MyLengthyFunction2(long param1, std::string tag)
{
...
}
void OnMyLengthyFunctionComplete3(int ret, UINT error_code,
ULONG_PTR completion_key)
{
ASSERT( 999 == ret );
ASSERT( 333 == completion_key );
...
}
void OnMyLengthyFunctionComplete4(UINT error_code,
ULONG_PTR completion_key)
{
ASSERT( 444 == completion_key );
...
}
void test()
{
afc::launch<afc::mfc_thread<> >(
&CMyWindow::MyLengthyFunction1, this, 123L, "job#1",
afc::on_completion( &OnMyLengthyFunctionComplete1, 111 ) );
afc::launch<afc::mfc_thread<> >(
&CMyWindow::MyLengthyFunction2, this, 123L, "job#1",
afc::on_completion( &OnMyLengthyFunctionComplete2, 222 ) );
afc::launch<afc::mfc_thread<> >(
&CMyWindow::MyLengthyFunction1, this, 123L, "job#1",
afc::on_completion( &CMyWindow::OnMyLengthyFunctionComplete3, this, 333 ) );
afc::launch<afc::mfc_thread<> >(
&CMyWindow::MyLengthyFunction2, this, 123L, "job#1",
afc::on_completion( &CMyWindow::OnMyLengthyFunctionComplete4, this, 444 ) );
}
};
completion_key
can be used to pass over any kind of user-defined information to the completion routine (from the afc::launch<>()
), or simply ignored if not required.
C. Exception Handler
One of the difficulties when trying to invoke a function asynchronously is the issue of how to handle exceptions which may be thrown in the middle of the target function call. If the target function is guaranteed not to throw one, it might not be our concern anymore, but there are many situations in which we must deal with exceptions.
afc, by default, sinks all exceptions thrown from the target function, and does not allow for them to propagate unhandled. When an unhandled exception is thrown from the target function, the completion routine will be called immediately with its UINT
type of error_code
set as AFC_ERROR_UNHANDLED_EXCEPTION
.
#include "afc.hpp"
class CMyWindow : public CWnd
{
int MyLengthyFunction(long param1, std::string tag)
{
throw "unhandled exception";
...
return 333;
}
void OnMyLengthyFunctionComplete(int ret, UINT error_code,
ULONG_PTR completion_key)
{
ASSERT( 0 == ret );
ASSERT( AFC_ERROR_UNHANDLED_EXCEPTION == error_code );
ASSERT( 777 == completion_key );
...
}
void test()
{
afc::launch<afc::mfc_thread<> >(
&CMyWindow::MyLengthyFunction, this, 123L, "job#1",
afc::on_completion( &CMyWindow::OnMyLengthyFunctionComplete, this, 777 ) );
}
};
However, such a default behavior can be easily customized and extended by providing a custom exception handler.
#include "afc.hpp"
class CMyWindow : public CWnd
{
int MyLengthyFunction(long param1, std::string tag)
{
throw "unhandled exception";
...
return 333;
}
struct MyExceptionHandler
{
template<typename TFxn>
int operator ()(TFxn fxn, UINT & error_code) const
{
try
{
return fxn();
}
catch(char const * e)
{
error_code = 444;
TRACE( _T("%s\n"), e );
return 333;
}
}
};
void OnMyLengthyFunctionComplete(int ret, UINT error_code,
ULONG_PTR completion_key)
{
ASSERT( 333 == ret );
ASSERT( 444 == error_code );
ASSERT( 777 == completion_key );
...
}
void test()
{
afc::launch<afc::mfc_thread<> >(
&CMyWindow::MyLengthyFunction, this, 123L, "job#1",
MyExceptionHandler(),
afc::on_completion( &CMyWindow::OnMyLengthyFunctionComplete, this, 777 ) );
}
};
An exception handler should be a callable according to the predefined function signature as well. It should be a template function, and the first template function argument is a null-nary function which represents the target function. Calling the nullnary function is translated into calling the target function with the packed arguments unpacked. The return of the exception handler and the error_code
will be passed over to the completion routine as input arguments.
- The Predefined function call signature of the exception handler:
R (TFxn, UINT &)
*R
should be implicitly convertible to the return type of the target function.
D. Inter-thread Communication #1 - From the Caller Thread
As previously mentioned, afc::launch<>()
creates a temporary afc
proxy object which delegates for the worker thread spawned. By accessing the member functions of the proxy object, it is possible to control the worker thread.
#include "afc.hpp"
class CMyWindow : public CWnd
{
int MyLengthyFunction(long param1, std::string tag)
{
...
return 333;
}
void test()
{
afc::proxy p = afc::launch<afc::mfc_thread<> >(
&CMyWindow::MyLengthyFunction, this, 123L, "job#1" );
TRACE( _T("Worker Thread ID: %x, Worker Thread Handle: %x\n")
, p.get_thread_id(), p.get_thread_handle() );
DWORD res = 0;
res = p.wait_with_message_loop( 3000 );
switch( res )
{
case WAIT_OBJECT_0:
break;
case WAIT_TIMEOUT:
p.abort();
break;
}
while( p.is_running() ) { }
}
};
All the available member functions of the afc::proxy
class are listed as shown below. By the way, afc::proxy
is CopyConstructible and Assignable so that it can be stored into STL containers.
namespace afc
{
class proxy
{
public:
HANDLE get_thread_handle() const;
DWORD get_thread_id() const;
BOOL abort() const;
bool is_running() const;
BOOL set_thread_priority(int priority) const;
int get_thread_priority() const;
DWORD suspend() const;
DWORD resume() const;
BOOL terminate(DWORD exit_code =
AFC_EXIT_CODE_TERMINATION) const;
DWORD wait(DWORD timeout) const;
DWORD wait_with_message_loop(DWORD timeout) const;
};
}
Calling abort()
causes a thread specific abort event synchronization object to become signaled, and the target function which is running in the specific worker thread may check the event object to decide whether or not to abort the execution. It will be illustrated below.
E. Inter-thread Communication #2 - From the Worker Thread
Thread specific local storage (TLS) is leveraged to make the worker thread be able to access and to communicate with the caller thread.
#include "afc.hpp"
class CMyWindow : public CWnd
{
int MyLengthyFunction(long param1, std::string tag)
{
for(int i = 0; i < INT_MAX; ++i)
{
if( afc::thread_specific::check_abort() )
{
return 999;
}
...
}
return 333;
}
void OnMyLengthyFunctionComplete(int ret, UINT error_code,
ULONG_PTR completion_key)
{
ASSERT( 999 == ret );
ASSERT( 777 == completion_key );
...
}
void test()
{
afc::proxy p = afc::launch<afc::mfc_thread<> >(
&CMyWindow::MyLengthyFunction, this, 123L, "job#1",
afc::on_completion( &CMyWindow::OnMyLengthyFunctionComplete, this, 777 ) );
p.abort();
}
};
Two thread specific singleton accessors are available:
namespace afc
{
class thread_specific
{
public:
static bool check_abort();
static HANDLE get_caller_thread_handle();
};
}
In order to make the scoped static initialization, which is used internally to implement the singleton pattern thread-safe, afc::launch<>()
is designed to guarantee that the worker thread procedure is commenced, and all necessary initialization of the thread specific local storage is completed before it returns.
If the above singleton accessors are called from the main thread (i.e., non-afc thread), the request will be simply ignored and will return false
and NULL
, respectively.
F. Thread Collector
The last, but not the least, issue is how to make sure all the spawned afc threads are safely terminated before the main program exits. afc::thread_collector
is designed to manage and to help clean up the afc threads which were launched through afc::launch<>()
. The thread collector is similar to the garbage collector, but instead of collecting the garbage memory, it collects the garbage resources for a specific thread which were assigned when it was spawned.
#include "afc.hpp"
#include "afc_thread_collector.hpp"
class CMyApp
{
void InitInstance()
{
...
afc::thread_collector::init();
}
};
class CMyWindow : public CWnd
{
int MyLengthyFunction(long param1, std::string tag)
{
...
}
void test()
{
afc::proxy p = afc::launch<afc::mfc_thread<> >(
&CMyWindow::MyLengthyFunction, this, 123L, "job#1" );
afc::thread_collector::contract( p );
}
};
Any afc
proxy that is contracted with afc::thread_collector
is guaranteed to be collected when the program exits. When the program exits, afc::thread_collector
will signal abort the events for each afc thread in the list kept internally, then wait for the predefined timeout period (AFC_THREAD_COLLECTOR_WAIT_TIMEOUT
= 5000 milliseconds, by default). When the complete timeout period is elapsed, but there are some afc threads still alive and running, afc::thread_collector
will force to terminate those leftover threads.
Since afc::thread_collector
uses the scoped static initializer to implement the singleton pattern, it may not be thread-safe in multithreading. To make it thread-safe, afc::thread_collector::init()
should be called in the main thread before creating a second thread which may use a service of afc::thread_collector
.
namespace afc
{
class afc_thread_collector
{
public:
static unusable init();
static void contract(afc::proxy const & p);
static void recede(afc::proxy const & p);
};
}
Notes
1. Remeber that the target function and the completion routine, if specified, are executed in the context of the worker thread.
Since it becomes so easy to call a function asynchronously, we might forget the fact that those functions are executed in a different thread context from the main thread. When using afc with frameworks that require to initialize some thread specific local storage for its own sake, you should pay cautious attention on it.
#include "afc.hpp"
class CMyWindow : public CWnd
{
int MyLengthyFunction(long param1, std::string tag)
{
CWnd * pWnd1 = CWnd::FromHandle( this->m_hWnd );
ASSERT( pWnd1 != this );
Attach( Detach() );
CWnd * pWnd2 = CWnd::FromHandle( this->m_hWnd );
ASSERT( pWnd2 == this );
return 0;
}
void test()
{
afc::launch<afc::mfc_thread<> >(
&CMyWindow::MyLengthyFunction, this, 123L, "job#1" );
}
};
2. Use a synchronization object to access member variables thread-safe.
For the same reason, we might forget that it requires a lock object to synchronize the access to the member variables if they are accessed either in the target function or in the completion routine.
#include "afc.hpp"
class CMyWindow : public CWnd
{
std::map<long, std::string> myMap_;
mutex lock_;
int MyLengthyFunction(long param1, std::string tag)
{
lock_.acquire();
myMap_[param1] = tag;
lock_.release();
return 0;
}
void test()
{
afc::launch<afc::mfc_thread<> >(
&CMyWindow::MyLengthyFunction, this, 123L, "job#1" );
lock_.acquire();
myMap_[123L] = "job#1";
lock_.release();
}
};
3. Do not pass over the pointer or reference to the local variable and use 'pass by value' semantics.
When one tries to convert a synchronous function call into the equivalent asynchronous function call, using afc, he or she may make a mistake like the one illustrated below, easily.
int MyOriginalFunction(std::string const & name)
{
...
}
void test(std::string const & name)
{
MyOriginalFunction( name );
}
We can make a call to MyOriginalFunction()
asynchronously using afc, as shown below.
#include "afc.hpp"
int MyOriginalFunction(std::string const & name)
{
...
}
void test(std::string const & name)
{
afc::launch<crt_thread<> >( &MyOriginalFunction, name );
}
Can you see the problem? It isn't easy to identify it at first sight, but if you look at the example carefully again, you will probably notice that it is passing over a reference to the local variable unintentionally.
void test(std::string const & name)
{
afc::launch<crt_thread<> >( &MyOriginalFunction, name );
return 0;
}
void test_all()
{
std::string myName = "Jae";
test( myName );
}
If test_all()
returns before the asynchronous function call to MyOriginalFunction()
is completed, it is highly likely to access an invalid memory space where myName
was allocated on the stack memory in the local function scope of test_all()
.
Changing the function signature to use 'pass by value' semantics will remedy this situation.
#include "afc.hpp"
int MyOriginalFunction(std::string name)
{
...
}
void test(std::string const & name)
{
afc::launch<crt_thread<> >( &MyOriginalFunction, name );
}
void test_all()
{
std::string myName = "Jae";
test( myName );
}
4. afc is compiled and tested based on Boost 1.33.1.