Intent
Encapsulating Win32 threads in a C++ class, easy to subclass and reuse. Hide the details of threads from users so they can focus on the project details.
Motivation
Object oriented languages like C++ have their strength in their ability to encapsulate the representation and implementation of an object, so that programming is focused on a higher level. We say we're programming at interface level rather than at function level. However most OSes haven't been design with C++ in mind, they were usually implemented using non-OO
approaches. That's why sometimes it can be tricky to encapsulate some platform dependent resources, like threads for instance. My approach covers Win32 threads.
Win32 threads
To create another thread in the same process, one has in Win32 couple of API functions that handle threads. However they are C and not C++ API. We can easily notice C idioms like callback functions, conversions to and from
void*
to other types and so on. Let's take a look at how CreateThread
, the API function that creates a thread in Win32 looks like. It's prototype it's shown below:
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes,
DWORD dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId
);
lpStartAddress is a pointer to a callback function that will run in the new thread, and
lpParameter is a parameter of type void* passed to the new thread.
Passing a pointer to a callback function however it's not in the spirit of OOP, and it comes like a serious impediment if we want to encapsulate threads in classes. The callback function required to be passed to
CreateThread
looks like this:
DWORD WINAPI ThreadProc(
LPVOID lpParameter
);
We'll notice this prototype will keep us from having this callback function as a member of a class, as it's member functions are passed a hidden parameter: this. What's to be done then? Did we fail miserably? Not yet. We cannot use member function and we've seen why, however classes have static methods, which have single instances for all objects. They are connected to classes rather than to objects. That's why they are not passed this as a parameter. So a static function becomes an interesting candidate for a callback function. However there's one small problem, if we have our thread in a static method, then no matter how many objects of that class we'll have, there will be only one thread, as a static method has a single instance per class. This is not what we want. We want our working thread to be a member method, easy to override by subclasses, and all this workaround to be transparent for the clients. Can we do that?
Yes we can. If you look at ThreadProc
, you'll notice it can be passed one
void*
parameter. Nothing prevents us from sending it (void*)this, and in
ThreadProc
we just call our working method, now that we have this pointer. The code for doing that will look like:
HANDLE CThread::CreateThread ()
{
return ::CreateThread ((NULL, 0, (unsigned long (__stdcall *)(void *))this->runProcess,
(void *)this, 0, NULL);
}
int CThread::runProcess (void* pThis)
{
return ((CThread*)(pThis))->Process();
}
int CThread::Process ()
{
}
So far so good. We managed to provide an encapsulation of threading mechanism, so users will simply have to implement their own Process, and then call
CreateThread
member function. It's even easier to provide reusability, as a user can simply inherit from our class defined above,
CThread
, and simply implement Process, and then call CreateThread
, and they have a thread simple as that. However there's a small issue to note here:
Let's say we have a subclass of CThread
, named CMyThread
. In CreateThread
will convert this (which is of type
CMyThread*
) to void* and pass it to runProcess
, where we reconvert this to
CThread
. C++ standard states that if you convert a type X* to void*, then only a conversion back, to the same type
X*
is permitted. Other conversions result in an undefined behaviour. That simply means we've done something wrong. How can we fix that? Well, with a small workaround.
struct workAround {
CThread* this_thread;
};
HANDLE CThread::CreateThread ()
{
workAround* wA = new workAround;
wa->this_thread = this;
return ::CreateThread ((NULL, 0, (unsigned long (__stdcall *)(void *))this->runProcess,
(void *)wa, 0, NULL);
}
int CThread::runProcess (void* pThis)
{
workAround* wA = (workAround*)pThis;
CThread* thread = wA->this_thread;
delete wA;
return thread->Process();
}
This time we're allright as we're converting to and from the same type (struct
workAround
).
Finally to be in the spirit of C++ we'll use C++ conversion, instead of C conversions.
Eg: instead of (void*)wA
we'll have static_cast<void*>(wA)
Conclusions
Due to the fact most nowadays OSes are not object oriented, as a C++ we usually have to find workarounds when we need to encapsulate platform dependent resources. We have to apply different tricks to achieve that, but once we encapsulated it, it's very simple to use and reusable with considerable less effort. Threads in Win32 are a good examples in that direction. You can further study the source code to get a deeper insight. Happy programming!