Introduction
Over the past few years, multithreaded apps have become the mainstay of my development repertoire. I've even found myself writing multithreaded apps where it wasn't needed. Worker threads allow Win32 applications to improve the user experience and increase performance.
For C++ programs and programmers, the Windows API's C interface doesn't necessarily fit well into new or existing class hierarchies. However, the thread concept has become the mainstay of parallel programming practice, and has become more appropriate in a wider and wider variety of circumstances.
In part 1 of this three article series, I will present my general thread class, explaining the design decisions behind it and how to use it to get the greatest effect.
The Thread Class
Before we get ahead of ourselves, let's take a look at the public interface of the Thread
class:
class Thread
{
public:
Thread();
virtual ~Thread();
void Suspend();
void Resume();
void Terminate();
bool IsThreadActive() const;
};
This rather basic interface is also rather clean. It exposes a fair amount of the operations the Win32 API allows on threads themselves, in a clean, C++-friendly class.
Now, you may be saying to yourself, "Hey? You said it was a complete interface. There isn't even a way to create a thread in there!". Ahh, we're getting there. But before we do, we have to go through template land.
That’s right, my implementation of the Thread
class uses a little template-magic. This magic is needed to allow us to use the same code among different client classes. After all, you wouldn't want to re-write the class for each client, would you?
So, let's look a little closer at a few particular lines of the source code.
template<class T, class P>
class Thread
{
public:
typedef void (T::*ThreadFunc)( P );
};
There are actually two different template parameters: T
and P
; or, put another way: Class and Parameter.
To invoke a method on an object, the compiler needs two things: the object's type (the T
in the template argument list), and the method's signature (which is composed of the return type, class, method name, and parameter list). For this implementation, the method is assumed to return nothing (void
) and takes one parameter (see article #3 in this series for a more general solution which doesn't make these assumptions).
The typedef
makes things very easy. It is a method typedef
, specifying that ThreadFunc
is a function pointer to a function of T
which takes a P
as a parameter and returns nothing. See why the typedef
makes it easier?
Now that we've pushed and shoved our way through template land, let's look at the meat of the Thread
class: the Run()
method.
bool Run( T* pClass, ThreadFunc pfFunc, P p );
That's it! That's the public interface for the Thread
class. Nothing too intense. And the implementation is not much more complex. So, with that, how do we use this new class?
Using the Thread Class
Let's look at an example on how to use the Thread
class:
class TestClass
{
...
private:
Lib::Thread<TestClass, int> m_thread;
};
In this example, the Thread
class should be used as a data member of another class (with a little modification, the class can be used as a stand-alone object; I will leave that as the dreaded and hated exercise to the reader............ for now). The enclosing class is the first template parameter to the Thread
class. This is needed for the compiler to invoke the correct method of the correct class. The second template parameter is the type of argument that will be passed to the thread method.
To start the thread, call the Thread::Run()
method:
m_thread.Run( this, &TestClass::DoWork, i );
This will create a new thread using the standard Win32 ::CreateThread(...)
method. This new thread will immediately turn around and call the method specified to the Thread::Run()
method (I know it may not look like it, but we're specifying a function in all those arguments).
The three arguments to the Thread::Run()
method are:
- The object we will be invoking the method upon
- The method we will be invoking
- The parameter we will pass to the method
What do we need to specify a function call? From the compiler's perspective, they are return type, class, method name, and parameter list. From the runtime's perspective, we also need an object. Since the return type is hard coded, and the class and parameter list is specified in the template argument, all the compiler needs is a method name, and the runtime needs an object. Guess what? Those are the first two parameters of the Thread::Run()
method (parameter 2 and 1, respectively).
After the thread is called, it (essentially) turns around and calls:
this->DoWork( i );
As soon as this method exits, the thread is finished with its work, and it exits as well.
Conclusion
The Thread
class is rather basic, but very useful. It provides a C++ wrapper for the powerful Win32 C Threading API. This particular implementation makes a few assumptions: the Thread
class must be a member of an enclosing class, the return type must be void
, and the method takes a single parameter.
These assumptions may be less than ideal for the time, so article #3 will show you how to get around these assumptions using more template magic. (Trust me, it won't be that painful.)
But first, article #2 will show you how to extend the Thread
class into the most useful threading paradigm I have ever come across.
History
- 2006.11.21 - First revision.