Introduction
The following article discusses a simple task/worker implementation. It trails along the lines of providing asynchronous "fire & forget" style mechanism for your application/object. It is assumed that the reader has knowledge about the following TR1 concepts; callable objects (std::tr1::function
), function binding (std::tr1::bind
). Without these pre-requisites, the article may be confusing.
Background
A lot of code that I've come across involving threads usually exhibit a "is-a" relationship between the object and the thread. This basically means the object "is a" thread. In some cases, it just makes sense to realize a "has-a" relationship between the object and its thread. On the surface, both may seem to achieve the same functionality but the latter provides a more modular approach.
Conventional coding practice involves, deriving your object off of some "CThread
" class and implementing a "Run()
" function of some sort which encapsulates the quantum of asynchronous work that needs to be accomplished. The "is-a" approach will do in most cases, but what if you could decouple the two? Design an object as you normally would do, write code for it and then attach asynchronous functionality to any function in the object.
Conventional usage:
class threaded_object: public my_base_thread_class
{
public:
threaded_object(){};
~threaded_object(){};
protected:
virtual int run()
{
return 0;
}
}
and the usage would be:
threaded_object my_object;
my_object.create_thread(); >-- This would create the thread and call the Run() function.
Let's look at how this is done a little differently using the Generic Task/Worker Implementation. The Generic Task/Worker object is a simple stub that executes callable objects, representing a quantum of work, on a separate thread. The class is presented inside the 'utility
' namespace and is called 'generic_worker
' and the type of callable object that it can execute is represented by the function type 'utility::generic_worker_task
'.
namespace utility
{
template<t_fn>
class generic_worker_t
typedef std::tr1::function<void()>generic_worker_task;
typedef generic_worker_t<generic_worker_task>generic_worker;
}
The 'generic_worker
' object expects the passed in callable objects to be of the type 'generic_worker_task
' which takes no arguments and returns nothing. Any function/member function in your class can be converted into a callable object using an operation known as 'bind
' (std::tr1::bind
). The bind
operation returns a callable object with pre-defined arguments already bound to the function. The only problem is with the return value. Functions in your class may return values and callable objects created from these functions cannot be contained in a 'generic_worker_task
' type object due to its return value. For this, can you easily templatize 'generic_worker_t
' to accept the appropriate callable object type. For instance, if your functions return 'int
', you can specialize 'generic_worker_t
' as shown below:
typedef std::tr1::function<int()>generic_worker_task_int;
typedef generic_worker_t<generic_worker_task_int>generic_worker_int;
Using the Code
Let's assume you have a class of the following form:
class my_object
{
public:
int function_a();
int function_b();
int function_c();
}
Assume that each of these functions takes a long time to complete. For example, imagine you want to invoke these functions directly from a GUI thread and if you do so, it causes the message loop to stop pumping messages and blocks, making the UI non-responsive thereby requiring this work to be offloaded to another thread. The example stated is just one of the many use cases.
Step 1: Create your templatized generic worker object. In this case, all the functions return int
, so the generic_worker
object will be specialized with a callable object that takes no arguments and returns an int
.
typedef std::tr1::function<int()>generic_worker_task_int;
typedef generic_worker_t<generic_worker_task_int>my_generic_worker;
Step 2: Include this worker in your main object:
class my_object
{
public:
int function_a();
int function_b();
int function_c();
protected:
my_generic_worker async_obj;
}
Step 3: Create stubs for asynchronous behavior where the async_[function_name]
represents the non-blocking or asynchronous version of the function. Only create stubs for those functions you need asynchronous behavior.
class my_object
{
public:
int async_function_a(int arg1);
int async_function_b(bool arg1);
int function_a(int arg1);
int function_b(bool arg1);
int function_c();
}
Step 4: Implement the stubs. This is where you'll use the bind operation to bind function arguments.
Depending on how you want the passed in callable objects pointers to be handled, you can either call the 'do_work
' function that does not delete the passed in callable object pointer or 'do_work_and_clean()
' function that deletes the passed in callable object.
int my_object::async_function_a(int arg1)
{
generic_worker_task_int* p = new( generic_worker_task_int );
if( p )
{
*p = std::tr1::bind(&my_object::function_a,this,arg1);
async.do_work_and_clean(p, THREAD_PRIORITY_NORMAL);
}
return 1;
}
int async_function_b(bool arg1)
{
generic_worker_task_int* p = new( generic_worker_task_int );
if( p )
{
*p = std::tr1::bind(&my_object::async_function_b,this,arg1);
async.do_work_and_clean(p, THREAD_PRIORITY_NORMAL);
}
return 1;
};
Step 5: Invoking the async stub will now execute the method asynchronously.
my_object instance_of_my_object;
instance_of_my_object.async_function_a(1);
The async_
version of the calls returns to the caller and the worker threads carry over the actual task. It's up to the developer of the object to design the parent object for multithreading taking care of concurrency issues and such. The 'generic_worker
' does not orphan any running tasks and will wait for all its task threads to complete before destruction.
The 'generic_task
' object may require synchronization and the sections that need to be concurrent are marked by comments. It may be worthwhile to protect these sections by adding locks/critsecs.
History