It's obviously a good idea to encapsulate a thread as an object. It is less obvious how to get all the interfaces right.
Developing software for modern systems can be a complex undertaking. Software systems are large, have many components, and are often multithreaded, especially as multiprocessor systems become more and more common. To help manage this complexity, object-oriented strategies, and object oriented-languages such as C++, have been developed.
C++ has no language constructs for threading, so developers must use system libraries such as pthreads on UNIX-like systems or functions such as CreateThread as part of the Win32 API. These libraries and functions, however, are not class-based but contain C-style mechanisms. In order to more easily apply an object-oriented design methodology, it is desirable to treat threads as objects, so some method of encapsulating thread API functions is needed. Once encapsulated, active objects containing threads can be used much the same way as passive objects.
One such encapsulation is shown in Listing 1. I omit the code usually needed in a production system, such as thread control or error checking. Including it would only complicate this discussion.
Class Thread contains a public member function Create, a protected member function Run, and a destructor. Create calls the pthread function pthread_create to create a new thread in the joinable state and start it running by invoking the function thread_func. A pointer to the Thread object is the last argument to pthread_create. It is passed as the pointer argument to thread_func. You can later synchronize with a thread in the joinable state by calling pthread_join, as is done here in the destructor. A thread may also be created in the detached state, or later changed to that state, which does not require a join to be performed.
The thread function plays an important role in thread encapsulation. Instead of containing the thread behavior, it makes use of the object pointer to call member function Run in class Thread. This allows the behavior of the thread to be determined by the thread object, not separately. Here, Run is implemented by class Thread, though it could also be made virtual and implemented by subclasses of Thread if only one thread in the object is desired. If the object requires multiple threads, it may be more convenient to directly implement multiple Run-style member functions as needed, rather than use Thread as an abstract base class.
It is important to note that the thread function must be a C-style function, not a member function. Hence the need for the roundabout way of calling Run. To reduce namespace pollution, thread_func could be a static function in class Thread, but the same problem remains. It cannot be an ordinary member function.
This structure is fine for a basic thread class, but suppose the class requires a template parameter. Just such a case occurred when I developed a particular object for a code library for my company. A problem now arises, since the encapsulation above no longer works, as shown in Listing 2.
In the non-template case, the thread function uses the passed this pointer of the object to call its Run member function. This pointer is received as a void *, which is safely cast to a Thread * to make the call to Run. In the template case, it must be cast to a Thread<T> * and this is where the problem lies. The thread function must now become a template function itself (as it needs a generic type parameter T). But as I pointed out above, the thread function must be a C-style function to be compatible with the thread library.
One way to overcome this problem is to use an intermediate object to wrap the Thread pointer, as shown in Listing 3. The Thread pointer wrapper is composed of two parts, a base class and a subclass. The base class Wrapper contains a single method Wrap and a virtual destructor. Wrap is pure virtual, which is the basis of the trick.
WrapperSub inherits from Wrapper, but unlike its base class, it is a template class with the same template parameters as Thread. The purpose of WrapperSub<T> is twofold. It first stores Thread's this pointer when it is constructed for later use. Since it has the same template parameters, storing a Thread<T> * can be done without difficulty. It also provides a means to call the Run member function in Thread. That is, WrapperSub<T> implements a function Wrap which now has the same code as thread_func had previously. It uses the stored this pointer to call Run in Thread. It is important to recognize that Wrap in class WrapperSub<T>, even though WrapperSub<T> is a template class, still overloads Wrap from its non-template base class Wrapper.
The call to pthread_create is modified to not pass this directly, but instead a dynamic instantiation of WrapperSub<T>. Keep in mind that WrapperSub<T> stores the this pointer of Thread.
The thread function has also changed. The pointer received is to an instance of class WrapperSub<T>, but the pointer is safely cast to one pointing to class Wrapper, syntactically eliminating the template parameter and allowing thread_func to remain a C-style function. The function Wrap is called from that pointer, but as the Wrap function in WrapperSub<T> overrode the Wrap function in Wrapper, it is Wrap() of WrapperSub<T> that is called. This function then uses the stored this pointer and calls Run in Thread<T> as is desired. When Run returns, Wrap returns, and the instance of WrapperSub<T> is freed.
Although slightly cumbersome, this method does solve the template problem and relies on the language features of C++ to do it. This trick is also applicable to other C-based threading libraries. For instance, you can use the same structure to encapsulate Win32 API thread creation by eliminating pthread_join, replacing pthread_create with _beginthread, and changing the return type of thread_func to void instead of void *. I'm certain this isn't the only way to address the problem, and I'd be interested in learning of others.
Charles Calkins is Vice President of Applied Intelligence, Inc. a St.Louis-based consulting company specializing in software development. In the last few years he has written software for a range of products including 3-D model navigators, a telephony system, and an information kiosk while working in St. Louis. He can be reached at calkinsc@applied-intelligence.com and via his web site at http://www.applied-intelligence.com/~calkinsc.