Click here to Skip to main content
15,881,092 members
Articles / Desktop Programming / MFC
Article

Asynchronous Function Calls: Working with Win32 threads made easy

Rate me:
Please Sign up or sign in to vote.
4.20/5 (16 votes)
7 May 2007CPOL9 min read 37.7K   613   21   2
An article on the new approach to utilize Win32 threads in a more intuitive manner.

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)
  {
    // Parameters packing.

    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)
  {
    // Parameters unpacking.

    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()
  {
    // 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 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<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 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();
        // (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 ) );
  }
};

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 );
    // 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 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
{

// 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 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() )
      { // 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 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
{

// 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 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() ); // 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 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) // '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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Other
Canada Canada
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
RantAFC library crashes my application. Pin
Oleksandr Dodatko14-Jul-10 1:25
Oleksandr Dodatko14-Jul-10 1:25 
GeneralVery nice, thanks Pin
Andreone13-Aug-09 2:16
Andreone13-Aug-09 2:16 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.