10 votes for this article.
Popularity: 3.69. Rating: 3.69 out of 5.

Introduction

When we create a worker thread, our intention is to perform a task in the separated 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 limits get into it due to the limits of the relevant APIs as well as issues which we must concern about. Some of these issues are simple and trivia while others are subtle and often really hard to identify the source of the problem at all, and it is the nature of multithreading.

But isn't what we've been doing for so many years fits into the same script thus a boilerplate code snippet was repeated over and over again? We learned how to use ::CreateThread(), _beginthread[ex]() and AfxBeginThread() to spawn a new worker thread and how to pack and unpack set of information to pass over to the worker thread through void pointer.

Let's assume that we have a lengthy synchronous target function which is required to be executed in the separated thread 'asynchronously'.

class CMyWindow : public CWnd
            {
            int MyLengthyFunction(long param1, std::string tag)
            {
            ...
            }
            };
            

In order to execute the synchronous function in a separated thread, we will need to use one of thread creation APIs.

Collapse
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)
            {
            // Parameters packing.
            std::auto_ptr
             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)
            {
            // Parameters unpacking.
            std::auto_ptr
             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 suggests one of the frequent thread creating scenario to perform some lengthy task in the separated worker thread. Since 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 requiring our further attention such as return value handling, exception handling, thread synchronization and so on. While one 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 issues ignorantly in turns to create 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, providing that creating a worker thread is all about for calling a function asynchronously. They are no better than calling raw thread APIs in my opinion.

Using afc library, it becomes extremely easy to call a function asynchronously in the separated thread without blocking the main thread. Forgets about the ancient myth saying that the 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()
            {
            // MyLengthyFunction( 123L, "job#1" );
            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 afc::launch<>() helper function template, a thread trait should be explicitly specified as the template parameter. There are three thread traits provided for afc library; afc::win32_thread, afc::crt_thread and afc::mfc_thread which corresponds to ::CreateThread(), _beginthreadex() and AfxBeginThread() respectively. If any of CRT function need to be called in the target function which is executed in the separated worker thread, you must use either crt_thread or mfc_thread. In the same context, if any of MFC function need to be called, you are only allowed to use mfc_thread otherwise the initialization of the necessary data structures which need to be switch 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 parameter 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 the thread trait is required thus proper thread trait should be specified manually whenever afc::launch<>() is called.

Collapse
#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<win32_thread<> >( &my_function_may_use_crt, 456L, "job#4" );
            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<win32_thread<> >( &my_function_may_use_mfc, 789L, "job#7" );
            // afc::launch<crt_thread<> >  ( &my_function_may_use_mfc, 890L, "job#8" );
            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 separated thread or to be notified the event of the completion of the target function, uses 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 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 MyLengthyFunction() function call is completed in the separated worker thread, OnMyLengthyFunctionComplete() will be invoked with the first parameter specified as the return of MyLengthyFunction() function call.

The function signature for the completion routine is predefined and the completion routine whose function signature matches with the predefined should be provided through 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)
Collapse
#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 the user-defined information to the completion routine (from the afc::launch<>()) or simply ignored if not required.

C. Exception Handler

One of difficulty 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, 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.

Collapse
#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(); // (pMyWnd->*&CMyWindow::MyLengthyFunction)( 123L, "job#1" );
            }
            catch(char const * e)
            {
            error_code = 444;
            TRACE( _T("%s\n"), e ); // Traces "unhandled exception".
            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",  // Target function
            MyExceptionHandler(),                                // Exception handler
            afc::on_completion( &CMyWindow::OnMyLengthyFunctionComplete, this, 777 ) );
            }
            };
            

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 nullnary function which represents for the target function. Calling the nullnary function is translated into calling the target function with packed arguments unpacked. Return of the exception handler and error_code will be passed over to the completion routine as input arguments.

  • 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<>() create a temporary afc proxy object which delegates for the worker thread spawned. By accessing member functions of the proxy object, it is possible to control the worker thread.

Collapse
#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 ); // Waits the target function call to complete for 3000
            // milliseconds with the second message loop running.
            switch( res )
            {
            case WAIT_OBJECT_0: // Target function call is completed.
            break;
            case WAIT_TIMEOUT:  // Timeout.
            p.abort();        // 'Signals' the worker thread to abort.
            break;
            }
            while( p.is_running() ) { }
            }
            };
            

All the avaiable member functions of 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.

Collapse
namespace afc
            {
            // Synopsis
            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;                      // ::SetThreadPriority()
            int get_thread_priority() const;                                   // ::GetThreadPriority()
            DWORD suspend() const;                                             // ::SuspendThread()
            DWORD resume() const;                                              // ::ResumeThread()
            BOOL terminate(DWORD exit_code = AFC_EXIT_CODE_TERMINATION) const; // ::TerminateThread()
            DWORD wait(DWORD timeout) const;                                   // ::WaitForSingleObject()
            DWORD wait_with_message_loop(DWORD timeout) const;                 // AtlWaitWithMessageLoop()
            };
            }
            

Calling abort() causes a thread specific abort event synchronization object 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 able to access and to communicate with the caller thread.

Collapse
#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() )
            { // Abort event has been signaled in the caller thread. (2)
            return 999;
            }
            // Some lengthy operations.
            ...
            }
            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(); // Signals the worker thread to abort. (1)
            }
            };
            

Two thread specific singleton accessors are available.

namespace afc
            {
            // Synopsis
            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 return false and NULL respectively.

F. Thread Collector

The last, but not 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 cleaning up of the afc threads which were launched through afc::launch<>(). Thread collector is similar to 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 program exits, afc::thread_collector will signal abort event for each afc threads in the list kept internally then wait for the predefined timeout period (AFC_THREAD_COLLECTOR_WAIT_TIMEOUT = 5000 milliseconds default). When the complete timeout period is elapsed but there are some afc thread still alive and running, afc::thread_collector will force to terminate those threads leftover.

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
            {
            // Synopsis
            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 excuted 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 the different thread context from the main thread. When using afc with frameworks that requires 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() ); // Synchonizes the thread specific handle map.
            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 synchronization object to access member variables thread-safe.

For the same reason, we might forget that it requires 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 uses '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 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 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 the test_all().

Changing the function signature to use 'pass by value' semantics will remedy this situation.

#include "afc.hpp"
            int MyOriginalFunction(std::string name) // 'pass by value'
            {
            ...
            }
            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.