Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C++

A Generic Task/Worker Implementation

4.67/5 (2 votes)
1 May 2011CPOL4 min read 16.7K   511  
A generic task/worker implementation to provide asynchronous 'fire & forget' mechanism for your object

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:

C++
class threaded_object: public my_base_thread_class
{
    public:

    threaded_object(){};
    ~threaded_object(){};

    protected:

    // This would be the overloaded function from your base class
    // that needs to be implemented. This represents the work performed
    // by the worker.

    virtual int run()
    {
    /* do some work */
    return 0;
    }
}

and the usage would be:

C++
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'.

C++
namespace utility
{
    // The template class
    template<t_fn>
    class generic_worker_t

    // This represents the type of callable object ( the quantum of work ) 
    // that the generic_worker class
    // can execute
    typedef std::tr1::function<void()>generic_worker_task;

    // Specialized generic_worker
    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:

C++
typedef std::tr1::function<int()>generic_worker_task_int;

// Specialized generic_worker
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:

C++
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.

C++
typedef std::tr1::function<int()>generic_worker_task_int;

// Specialized generic_worker
typedef generic_worker_t<generic_worker_task_int>my_generic_worker;

Step 2: Include this worker in your main object:

C++
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.

C++
class my_object
{
public:

    // async stubs
    int async_function_a(int arg1);
    int async_function_b(bool arg1);

    // actual functions
    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.

C++
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.

C++
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

  • 4/28/2010 - Revision 1

License

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