Click here to Skip to main content
15,860,972 members
Articles / Programming Languages / C++
Article

A C++ Cross-Platform Thread Class

Rate me:
Please Sign up or sign in to vote.
4.56/5 (26 votes)
15 Nov 20034 min read 517.2K   1.8K   49   38
Write portable object-oriented threads that work on Win32 and Posix-Compliant systems without modification.

Introduction

First off, the "prerequisites" of this article: I give some brief examples of using a mutex and semaphore object, but I leave it up to the reader to understand exactly what they are. Also, you should have a good understanding of multithreading before reading this article.

"What makes this different than other thread classes?" you might ask. First off, it is portable (but you probably noticed that from the title) - but my favorite part is that it is efficient. Nowhere in this article or in my code will you use new, delete, malloc, free, etc... everything is done on the stack. I am still hard at work to make this class even faster, so check back every now & then for updates, assuming you enjoy the code.

Please let me know how you like this article & code and if you have any suggestion to make it better.

Features

  • Pure virtual interface to easily convert any class to a threaded class
  • Type abstractions for thread handles
  • No code change needed between supported platforms
  • Static functions for easy static thread creation
  • Basic portable synchronization objects
  • Tested under Win32 (95-XP), *Nix, and SunOS >5.7
  • Should work on any posix-compliant system that support standard semaphores

The Thread Class

Here is a quick run-down of the functionality available to the user:

template < typename Thread_T >
class Thread
{
  public:
     /* just an old habit of mine */
    typedef Thread_T          & Thread_R;
    typedef const Thread_T    & Thread_C_R;
 
    /* The handle type needed to use the auxiliary functions */
    typedef /*Platform-Specific*/ Handle;
 
    /* The type of the function that would be called by the static */
    /*  Create() method, also the type of the pure virtual ThreadMain */
    typedef void (/*VS: __cdecl*/ *Handler)( Thread_R );
    /* Why is Thread_T a reference? because the parameter is */
    /* copied before the thread function is called. */
 
    /* There is a specialized version of this class for Thread_T */
    /* of type void. In this version, the Handler function type */
    /* has no parameter, and the create functions also take no */
    /* parameters. I thought this would make things faster since */
    /* there is no temporary objects or semaphores to deal with, */
    /* but I experienced a slow-down for Thread<void>! Let me know */
    /* if you happen to have insight into this. */
  protected:
    /* This class is meant to be inherited, because it is useless */
    /* in the sense of having instances, not to mention the */
    /* pure virtual function... */
    Thread();
 
    /* Derived classes will have this function, and it will be called */
    /* as a new thread. */
    virtual void ThreadMain( Thread_R ) = 0;


 
    /* Static functions, for use within the thread */ 
 

    /* Called from within the thread to return resources to the OS */
    static void Exit();

    /* POSIX only - for threads created with CancelAsync = false, */
    /* tests to see if someone is trying to kill the current thread. */
    /* does nothing in Win32. */
    static void TestCancel();
 

    /* Returns a handle to the current thread, or a pseudo-handle */
    /* in Win32. Posix threads can Detach() themselves with this  */
    /* function, but you can't CloseHandle() on a pseudo-handle or */
    /* a duplicated handle. */
    static Handle Self();
 
  public:
 
    /* The static thread creation function. */
    /*  useful when you don't need a class for your thread. */
    /* Pretty self-explanatory. */
    /* CreateDetached: if true, the thread's 
            resources are automatically freed */
    /*   when it exits, but you cannot Join() it. */
    /* CancelEnable: if true, this thread can be Kill()'ed */
    /* CancelAsync: if true, The thread is terminated 
                                                immediately upon Kill() */
    /*  being called. if false, it will only 
                                   exit when TestCancel() is called. */
    /* Return Value: returns 0 on success, and errno on fail. */
    /*   H is set to the handle (or InvalidHandle on fail) if not zero. */
    static int Create(
      const Handler       & Function,
      Thread_C_R            Param,
      Handle  * const     & H               = 0,
      const bool          & CreateDetached  = false,
      const unsigned int  & StackSize       = 0,
      const bool          & CancelEnable    = false,   // UNUSED in Win32
      const bool          & CancelAsync     = false    // UNUSED in Win32
    );
 
    /* The in-class creation function. Same params, minus function ptr. */
    /* Same return value, too. */
    /* Thread<void>::Create() functions do not take a Param */
    int Create(
      Thread_C_R            Param,
      Handle  * const     & H               = 0,
      const bool          & CreateDetached  = false,
      const unsigned int  & StackSize       = 0,
      const bool          & CancelEnable    = false,   // UNUSED in Win32
      const bool          & CancelAsync     = false    // UNUSED in Win32
    ) const;
 
    /* Wait for thread with handle H to complete */
    /* returns EAGAIN, EINVAL on error or zero on success. */
    static int Join( Handle H );
    /* Forcefully terminate the thread 
            (win32) or cancel the thread (posix). */
    /* A better way to end your thread is 
            through some kind of external sync */
    /* mechanism. */
    /* returns zero on success or EINVAL */
    static int Kill( Handle H );
    /* Allow the system to reclaim the 
            thread's resources when it is done. */
    /* Join()ing will not work anymore (posix), 
            but you can devise another */
    /* mechanism if you like, maybe using mutexes. 
                         This closes the handle in Win32. */
    /* returns zero on success or EINVAL */
    static int Detach( Handle H );
};

As you might see, I stuck to the basics with this class. Some functions aren't implemented on Win32, but you can still pretend to use them since they are there, without worry of impact on code quality - that way you can know the specifics of how your thread will run on a non-Windows platform.

Like I said before, I don't use any virtual memory when creating the threads. Instead, I use a semaphore and a temporary object:

int Create( ... )
{
  Semaphore S;
  Temp_Object T( [ref to] S, [ref to] Parameter, this );



  if ( Call_To_OS_Thread_Create(Static_Function,(void *)&T) Succeeds )
  {
    /* wait for the thread to signal the semaphore */
    S.Wait();
    /* now we know that the thread function is done playing */
    /* with the stack */
    return OK;
  }

  return FAILED;
}


static void Static_Function( void *T )
{
  Thread *DestObj(((Temp_Object *)T)->ClassPtr);
  Thread_T Copy_Of_Parameter(((Temp_Object *)T)->ParamRef);
  /* cuases the create function to stop waiting */
  ((Temp_Object *)T)->Sem->Post();

  /* now we can call the overloaded thread function for DestObj */
}

As the pseudo-code shows, the create function can safely pass a pointer to the object on its stack, since it will not exit until the thread function is done with it. The create functions are also object-friendly because they only copy the Thread_T once. If you're a veteran template writer, you may have noticed that the layout of the class causes problems when Thread_T is a reference type. To get around this, just make a simple struct with a reference as a value in it, and you'll be back in business. Just a quick note: hopefully you are acquainted with mutexes and semaphores as I suggested you should be, above. I have also included some very simple platform-specific classes names Semaphore and Mutex which have some basic functions that behave as expected. For Mutex, these functions are: Lock, Unlock and Lock_Try (for >= Win98). For Semaphore, they are: Reset(value), Post (i.e. Release), Wait, Wait_Try and Value. Note that the Mutex class is not actually a mutex in Win32, but rather a critical section which is must faster than an inter-process mutex.

Usage

I hope these are enough to get you acquainted, along with the detailed description of the Thread member functions above.

/*
  Example1.cpp
 
  In this example, I will show you how to write your own class to define your
  own thread function that is tied to an object, along with one way to use a 
  semaphore and a mutex.
 
  Win32:
   Make sure to set "C/C++" -> "Code Generation" -> "Runtime Library" to
      - Multi-Threaded Debug, or
      - Multi-Threaded
 
  Solaris, Linux, Unix, [Posix Compliant]:
 
    compile as g++ Example1.cpp /usr/lib/libthread.so
 
    sometimes it's libpthread.so... try
 
        find /usr/lib -n "lib*thread*.so"
*/
 
#include <iostream>
using namespace std;
#include <time.h>
 
#include "Thread.h"
#include "Mutex.h"
 
int ii = 0;
 
/* the mutex that all threads will use */
Mutex ii_lock;
 
/* the semaphore that the Run() function waits on */
Semaphore sem_done;
 
const static int rep = 1000;
 
/* Our class that has Thread<int> 
     as a base class - int is the parameter type. */
class ThreadTester : private Thread<int>
{
  public:
    void Run()
    {
      /* create rep #threads with parameter i. */
      /* if this class was private Thread<void>, there would not be a */
      /* parameter. */
      /* real code should check for errors from Create(). */
      for ( int i = 0; i < rep; ++i )
        Create(i,0,true,2048);
 
      /* when sem_done.Post() is called below, this wait completes */
      /* and also decrements the value of sem_done back to zero. */

      sem_done.Wait();

      /* another way to do this might be to keep a list of all */
      /* the thread handles and call Join() for each one, */
      /* but I chose this way since the 
             number of threads is pre-determined. */
    }
 
  private:
    /* the thread entry function, which is part of this object. */
    /* for Thread<void>, there would be no parameter. */
    /* i is a reference because it is copied in the _real_ thread */
    /* initialization, and so this just points to it. handy for */
    /* structs and things other than integral data types. */
    void ThreadMain(int &i)
    {
      /* get mutually exclusive access to ii */
      ii_lock.Lock();
 
      ++ii;
 
      /* if this is the last created thread, increment the value */
      /* of sem_done to 1, which causes Run() to complete. */
      if ( ii >= rep ) sem_done.Post();
 
      /* let someone else pass ii_lock */
      ii_lock.Unlock();
 
    }
};
 
int main()
{
  clock_t c1, c2;
  ThreadTester Test;
 
  /* just to test efficiency. */
  c1 = clock();
  Test.Run();
  c2 = clock();
 
/* you will notice a MAJOR speed increase from win32 to solaris. */
/* I havent run this example on anything else, but I assume that */
/* win32 threads are either less efficient or the creation process */
/* is more "processor-friendly" since win32's aim isn't to create */
/* fast but to run fast (if that's really possible for win32 :P ) */
/* I get about ~200 for my winXP 850MHz Pentium III-M - */
/* clocked about !!!12000!!! for SunOS 5.8 on (I believe) 4 processors. */
/* single processor solaris - can't remeber results at the moment. */
  cout << (double)rep/((double)(c2-c1)/(double)CLOCKS_PER_SEC) <<
    " Threads per second!" << endl;
 
  #ifdef WIN32
  system("pause");
  #endif
  return 0;
}
 
/*
  Example2.cpp
 
  In this example, I will show you how to use the static thread functions
  instead of writing your own class, along with how to explicitly wait
  for a single thread to complete.
 
  Win32:
   Make sure to set "C/C++" -> "Code Generation" -> "Runtime Library" to
      - Multi-Threaded Debug, or
      - Multi-Threaded
 
  Solaris, Linux, Unix, [Posix Compliant]:
 
    compile as g++ Example1.cpp /usr/lib/libthread.so
 
    sometimes it's libpthread.so... try
 
        find /usr/lib -n "lib*thread*.so"
*/
 
#include <iostream>
using namespace std;
 
#include "Thread.h"
 
/* non-class thread function */
void MyThreadProc( int &I )
{
  for ( int i = 0; i < I; ++i );
}
 
/* Our parameter type is int */
typedef Thread<int> Thread_Int;
 
int main()
{
  Thread_Int::Handle H;
 
  /* I had to specify the explicit type of the function in order */
  /* to force the calling of the static overload. */
  if ( Thread_Int::Create((Thread_Int::Handler)MyThreadProc,100000000,&H) )
  {
    cout << "Creating of thread failed!\n";
  }
  else
  {
    cout << "Waiting...\n";
    /* this returns when the thread completes */
    Thread_Int::Join(H);
    cout << "Thread is done.\n";
  }
 
  #ifdef WIN32
  system("pause");
  #endif
  return 0;
}

I'm sure...

... That there's a lot more I could go in-depth about, with respect to threads, mutexes, semaphores and synchronization, but I chose to list this article as intermediate and hope the reader has read enough of the "Threads, Processes & IPC" section of CP to know what is going on. My aim was to show you a facility for easy portable multithreading with this particular class - I hope that I succeeded in my efforts and I look forward to your feedback.

Updates

  • Updates made on 11/17/03:
    • Another Win32 tweak - made the default create function CreateThread (after inspecting the _beginthread code). You can still specify the other methods... see Win32/Thread.h for details. (BTW, it is a little faster for you speed freaks out there :))
  • Updates made on 11/16/03:
    • Removed __stdcall on the Thread<>::Handler type - not necessary (Win32)
  • Updates made on 11/12/03:
    • Added functionality for Win32 Detach() - closes the thread's handle. Unfortunately, a thread cannot detach itself as in Posix threads, so the best use is in creation or outside of the thread.
    • Added the Self() function, which returns the thread's handle to itself. Only a pseudo-handle in Win32.
    • Increased speed even more by using the same semaphore to synchronize the creation of all threads, along with a static Mutex.
    • Explicitly specified the static thread init functions as __stdcall (Win32)
    • Added a StackSize argument to the create functions
    • Used _beginthreadex/_endthreadex instead of _beginthread/_endthread (Win32)

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Web Developer
United States United States
I am a student at Portland State University in Portland, Oregon majoring in Mathematics and minoring in Computer Science and Physics.

Comments and Discussions

 
GeneralMy vote of 5 Pin
philip_bennefall5-Apr-11 6:40
philip_bennefall5-Apr-11 6:40 
GeneralSolaris 8 compatibility Pin
f_randy9-Aug-06 2:00
f_randy9-Aug-06 2:00 
GeneralFail from "Example2.cpp" Pin
bluet97-May-06 20:16
bluet97-May-06 20:16 
GeneralReleaseSemaphore cannot be used for getting Value Pin
Alexey.br13-Mar-06 20:51
Alexey.br13-Mar-06 20:51 
QuestionLicence ? Pin
Gilius10-Jan-06 7:30
Gilius10-Jan-06 7:30 
Generalpatch for g++ 3.4.3 and 4.0.0 Pin
Martijn Hijdra17-Aug-05 21:26
Martijn Hijdra17-Aug-05 21:26 
GeneralRe: patch for g++ 3.4.3 and 4.0.0 Pin
Anonymous24-Aug-05 3:38
Anonymous24-Aug-05 3:38 
GeneralRe: patch for g++ 3.4.3 and 4.0.0 Pin
mleake10-Apr-06 10:31
mleake10-Apr-06 10:31 
GeneralRe: patch for g++ 3.4.3 and 4.0.0 Pin
Nirandas19-Mar-09 9:01
Nirandas19-Mar-09 9:01 
GeneralRe: patch for g++ 3.4.3 and 4.0.0 Pin
Nirandas19-Mar-09 9:13
Nirandas19-Mar-09 9:13 
QuestionA Bug with Win32? Pin
CPAVG8-Jan-05 0:09
CPAVG8-Jan-05 0:09 
GeneralI made it work in VC6.0, but........ Pin
CPAVG7-Jan-05 19:45
CPAVG7-Jan-05 19:45 
GeneralProblem compiling under Linux Pin
mathmoi25-Apr-04 17:16
mathmoi25-Apr-04 17:16 
GeneralRe: Problem compiling under Linux Pin
Phillip Sitbon5-May-04 7:31
Phillip Sitbon5-May-04 7:31 
GeneralRe: Problem compiling under Linux Pin
mathmoi9-May-04 5:16
mathmoi9-May-04 5:16 
GeneralRe: Problem compiling under Linux Pin
Anonymous9-May-04 13:57
Anonymous9-May-04 13:57 
GeneralRe: Problem compiling under Linux Pin
mathmoi9-May-04 14:55
mathmoi9-May-04 14:55 
GeneralRe: Problem compiling under Linux Pin
Phillip Sitbon11-Aug-04 16:54
Phillip Sitbon11-Aug-04 16:54 
Hi Mathieu,
I obviously haven't checked this article for a while Smile | :) I've been very busy beginning my graduate studies in computer science.

The answer to your question is yes, you can use my code anywhere, and some mention of my name & contact info (phillip@sitbon.net) is all I ask. This may change in the future, but I don't have time to decide how to handle that at the moment. I am interested in your project, so feel free to keep in touch as well.

Sorry for not keeping things up to date - I've been swamped. I have some new code to introduce, and I have finally decided that exceptions in my code isn't really a performance hit, so they are there as well - this greatly simplifies things.

good luck with the code!

- Phillip
GeneralRe: Problem compiling under Linux Pin
mathmoi11-Aug-04 17:09
mathmoi11-Aug-04 17:09 
GeneralRe: Problem compiling under Linux Pin
Phillip Sitbon9-May-04 13:59
Phillip Sitbon9-May-04 13:59 
GeneralVC++ 6.0 Pin
ansayre17-Nov-03 9:12
ansayre17-Nov-03 9:12 
GeneralRe: VC++ 6.0 Pin
Phillip Sitbon17-Nov-03 16:21
Phillip Sitbon17-Nov-03 16:21 
GeneralRe: VC++ 6.0 Pin
celeona20-Feb-04 6:42
celeona20-Feb-04 6:42 
GeneralRe: VC++ 6.0 Pin
Phillip Sitbon20-Feb-04 18:55
Phillip Sitbon20-Feb-04 18:55 
GeneralRe: VC++ 6.0 Pin
CPAVG6-Jan-05 20:54
CPAVG6-Jan-05 20:54 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.