Thread: Win32 Thread Object Model Revisted

  1. #1
    Registered User Codeplug's Avatar
    Join Date
    Mar 2003
    Posts
    4,981

    Win32 Thread Object Model Revisted

    Updated 12/15/04, 9:45am EST

    I once posted a Win32 thread object class implementation that I've referenced a few times here on the C Board and I've always thought that I could do better. So I did.
    Here is a summary of the changes for those who are interested in contrasting new code vs. old code.

    - Removed WINAPI from ThreadProc_t, it served no purpose.
    - Removed m_threadid member, that's what GetThreadId() is for.
    - Removed PriorityClass_t and SetPriority(), that's for process handles (oops).
    - [Get/Set]ThreadData() APIs will now cast the void* for you
    - Start() method now provides full access to CreateThread() API parameters.
    - Removed Stop() interface in favor of simpler [Get/Set]ExitFlag() interface.
    - Prevents assignment and copy construction.
    - Supports both function and member function threads with a single interface.
    - Supports both CRT and Win32 thread API's. (See this thread as to why this may be important.)

    Code:
    //-----------------------------------------------------------------------------
    // thread.h (use as you wish)
    #ifndef CP_THREAD_H
    #define CP_THREAD_H
    
    #include <windows.h>
    #include <assert.h>
    
    namespace CP_Thread
    {
    
    // Forward decl.
    class ThreadBase; 
    
    //-----------------------------------------------------------------------------
    // Compile time assertion utility
    template <bool assertion> struct compile_assert;
    template <> struct compile_assert<true> {}; // specialized on true only
    
    //-----------------------------------------------------------------------------
    // ThreadFunc_t - Thread function type used by the ThreadBase class. All 
    //                thread functions return DWORD and take a ThreadBase* 
    //                parameter.
    typedef DWORD (*ThreadFunc_t)(ThreadBase*);
    
    //-----------------------------------------------------------------------------
    // Base class for functor object which invokes the user [member] function
    struct ThreadFunctor
    {
        virtual DWORD Invoke(ThreadBase *t) = 0;
    };//ThreadFunctor
    
    //-----------------------------------------------------------------------------
    // Functor object for non-member functions
    struct NonMemFunctor : public ThreadFunctor
    {
        ThreadFunc_t m_tfp;
        NonMemFunctor(ThreadFunc_t tfp) : m_tfp(tfp) {}
        virtual DWORD Invoke(ThreadBase *t) {return m_tfp(t);}
    };//NonMemFunctor
    
    //-----------------------------------------------------------------------------
    // Functor object for member functions
    template <typename Obj_t>
    struct MemFunctor : public ThreadFunctor
    {
        typedef DWORD (Obj_t::*MemberFunc_t)(ThreadBase*);
        MemberFunc_t m_tfp;
        Obj_t *m_obj;
        MemFunctor(Obj_t *obj, MemberFunc_t tfp ) : m_tfp(tfp), m_obj(obj) {}
        virtual DWORD Invoke(ThreadBase *t) {return (m_obj->*m_tfp)(t);}
    };//MemFunctor
    
    //-----------------------------------------------------------------------------
    // ThreadBase - Provides interface for thread objects
    class ThreadBase
    {
    protected:
        //-------------------------------------------------------------------------
        // Virtual method for actually creating the thread. This is an abstract 
        // interface in order to accomadate both the CRT and Win32 API's for
        // creating threads.
        virtual HANDLE create_thread(DWORD flags = 0, LPSECURITY_ATTRIBUTES psa = 0, 
                                     DWORD stack_sz = 0) = 0;
    
        //-------------------------------------------------------------------------
        // Thread entry point for Win32 threads
        static DWORD WINAPI Win32ThreadProc(LPVOID pv)
        {
            ThreadBase *t = reinterpret_cast<ThreadBase*>(pv);
            return t->m_functor->Invoke(t);
        }//Win32ThreadProc
    
        //-------------------------------------------------------------------------
        // Thread entry point for CRT threads
        static unsigned __stdcall CrtThreadProc(void *pv)
        {
            ThreadBase *t = reinterpret_cast<ThreadBase*>(pv);
            return t->m_functor->Invoke(t);
        }//CrtThreadProc
    
        //-------------------------------------------------------------------------
        // Member data
        ThreadFunctor *m_functor; 
        HANDLE m_hthread;
        volatile void *m_thread_data;
        volatile LONG m_flags;
     
    public:
        //-------------------------------------------------------------------------
        // enum used by SetPriority() for type safety
        enum Priority_t
        {
            priority_above_normal  = THREAD_PRIORITY_ABOVE_NORMAL,
            priority_below_normal  = THREAD_PRIORITY_BELOW_NORMAL,
            priority_highest       = THREAD_PRIORITY_HIGHEST,
            priority_idle          = THREAD_PRIORITY_IDLE,
            priority_lowest        = THREAD_PRIORITY_LOWEST,
            priority_normal        = THREAD_PRIORITY_NORMAL,
            priority_time_critical = THREAD_PRIORITY_TIME_CRITICAL,
        };//Priority_t
    
        //-------------------------------------------------------------------------
        // Constructor
        ThreadBase() : m_functor(0), m_hthread(0), 
                       m_thread_data(0), m_flags(0) {}
     
        //-------------------------------------------------------------------------
        // Destructor - If the thread is running, it is killed (not good)
        virtual ~ThreadBase() 
        {
            if (m_hthread != 0)
            {
                if (IsRunning())
                {
                    assert(false); // thread shouldn't be running!
                    Kill(); // not good
                }//if
    
                ::CloseHandle(m_hthread);
            }//if
    
            delete m_functor;
        }//destructor
    
        //-------------------------------------------------------------------------
        // Sets the thread entry point function. This is the function which is 
        // called when the thread is started.
        //   fun - pointer to function to execute in this thread's context
        void SetThreadEntry(ThreadFunc_t fun)
        {
            delete m_functor;
            m_functor = new NonMemFunctor(fun);
        }//SetThreadEntry
    
        //-------------------------------------------------------------------------
        // Use this version to run object member functions in the context of this 
        // thread.
        //   obj - pointer to struct or class instance to run the member function 
        //         thread on
        //   fun - pointer to object member function to run in this thread's 
        //         context
        template <typename Obj_t>
        void SetThreadEntry(Obj_t *obj, DWORD (Obj_t::*fun)(ThreadBase*))
        {
            delete m_functor;
            m_functor = new MemFunctor<Obj_t>(obj, fun);
        }//SetThreadEntry
     
        //-------------------------------------------------------------------------
        // HANDLE operator for access to the thread handle
        operator HANDLE() {return m_hthread;}
     
        //-------------------------------------------------------------------------
        // Set a custom data value in the thread. Member templates  are used for 
        // convenience but only pointer-sized data types or smaller may be used. 
        template <typename T>
        void SetData(T data) 
        {
            // only pointer-sized data or smaller allowed
            compile_assert<sizeof(T) <= sizeof(void*)>();
            InterlockedExchangePointer(&m_thread_data,
                reinterpret_cast<void*>(data));
        }//SetData
        
        // For const pointer types
        template <typename T>
        void SetData(const T *data) 
        {
            InterlockedExchangePointer(&m_thread_data, 
                reinterpret_cast<void*>(const_cast<T*>(data)));
        }//SetData
    
        //-------------------------------------------------------------------------
        // Get a custom data value in the thread. Member templates  are used for 
        // convenience but only pointer-sized data types or smaller may be used. 
        // The same type used in the SetData() call should also be used in
        // the GetData() call.
        template <typename T>
        void GetData(T &data) const
        {
            // only pointer-sized data or smaller allowed
            compile_assert<sizeof(T) <= sizeof(void*)>();
            InterlockedExchangePointer(&data, m_thread_data);
        }//GetData
    
        //-------------------------------------------------------------------------
        // Set user-defined flags for this thread. Subsequent calls to GetFlags() 
        // will return the last value set by this method. The default value is 
        // zero. Returns the previous flags value.
        LONG SetFlags(LONG flags)
        {
            // casts needed by dev-c++
            return InterlockedExchange((LONG*)&m_flags, flags);
        }//SetFlag
        
        //-------------------------------------------------------------------------
        // Set user-defined flags for this thread. The default value returned is
        // zero, otherwise it's the last value set by SetFlags().
        LONG GetFlags() const 
        {
            // LONG cast needed by dev-c++
            return InterlockedExchange((LONG*)&m_flags, (LONG)m_flags);
        }//GetFlag
    
        //-------------------------------------------------------------------------
        // Returns false if the thread hasn't been created. Otherwise, the current
        // execution state of thread is returned. Note that this will return true 
        // if the thread is suspended.
        bool IsRunning() const
        {
            if (m_hthread == 0)
                return false;
            return ::WaitForSingleObject(m_hthread, 0) == WAIT_TIMEOUT;
        }//IsRunning
    
        //-------------------------------------------------------------------------
        // Start the threads execution. Returns true if the thread is already 
        // running or if the thread was started successfully. Returns false if the
        // thread could not be created.
        //    flags, psa, stack_sz - see CreateThread() documentation
        bool Start(DWORD flags = 0, LPSECURITY_ATTRIBUTES psa = 0, 
                   DWORD stack_sz = 0)
        {
            assert(m_functor != 0);
    
            if (IsRunning())
                return true;
    
            if (m_hthread != 0)
                ::CloseHandle(m_hthread);
    
            m_hthread = create_thread(flags, psa, stack_sz);
            
            return m_hthread != 0;
        }//Start
    
        //-------------------------------------------------------------------------
        // Wait for thread execution to complete. Returns true if the thread
        // execution has completed. Returns false if the thread was never started
        // or if the timeout occured before thread execution was complete.
        //    timeout - number of milliseconds to wait before giving up. If not
        //              specified, an infinite timeout is used.
        bool Join(DWORD timeout = INFINITE)
        {
            if (m_hthread == 0)
                return false;
            return ::WaitForSingleObject(m_hthread, timeout) == WAIT_OBJECT_0;
        }//Join
    
        //-------------------------------------------------------------------------
        // Terminate the currently running thread. Returns true if the thread is 
        // not running or if the thread was terminated successfully.
        // NOTE: This is a dangerous function that should only be used in the most
        //       extreme cases. 
        bool Kill(DWORD exitcode = 0xDEADBEAF)
        {
            if (!IsRunning())
                return true;
            return ::TerminateThread(m_hthread, exitcode) == TRUE;
        }//Kill
    
        //-------------------------------------------------------------------------
        // Get the DWORD return value of the thread function. Returns false if the
        // thread was never started or the exit code could not be retrieved.
        //    exitcode - reference parameter to receive the exit code
        bool GetExitCode(DWORD &exitcode) 
        {
            if (m_hthread == 0)
                return false;
            return ::GetExitCodeThread(m_hthread, &exitcode) == TRUE;
        }//GetExitCode
    
        //-------------------------------------------------------------------------
        // Set the threads priority. Returns false if the thread isn't running. 
        // Returns true if the proirity of the thread is set successfully.
        //    p - A value from the Priority_t enum.
        bool SetPriority(Priority_t p)
        {
            if (!IsRunning())
                return false;
            return ::SetThreadPriority(m_hthread, p) == TRUE;
        }//SetPriority
    
    protected:
        // don't allow copies or assignment
        ThreadBase(const ThreadBase&); // no implenentation
        ThreadBase& operator=(const ThreadBase&); // no implenentation 
    };//ThreadBase
    
    //-----------------------------------------------------------------------------
    // Enumeration of supported thread API's - each enumeration represents a
    // template specialization of Thread<>
    enum ThreadAPI
    {
        win_thread_api,
        crt_thread_api,
    };//ThreadAPI
    
    //-----------------------------------------------------------------------------
    // Thread<> template - this class is specialized for each ThreadAPI enum 
    template <ThreadAPI thread_api> class Thread;
    
    //-----------------------------------------------------------------------------
    // Thread<> specialization which uses the Win32 API
    template <>
    class Thread<win_thread_api> : public ThreadBase
    {
    protected:
        //-------------------------------------------------------------------------
        // create_thread using Win32 thread API
        virtual HANDLE create_thread(DWORD flags = 0, LPSECURITY_ATTRIBUTES psa = 0, 
                                     DWORD stack_sz = 0)
        {
            DWORD thread_id;
            return ::CreateThread(psa, stack_sz, &Win32ThreadProc, 
                                  this, flags, &thread_id);
        }//create_thread
    };//Thread<win_thread_api>
    
    //-----------------------------------------------------------------------------
    // Thread<> specialization which uses the CRT API
    template <>
    class Thread<crt_thread_api> : public ThreadBase
    {
    protected:
        //-------------------------------------------------------------------------
        // create_thread using CRT thread API
        virtual HANDLE create_thread(DWORD flags = 0, LPSECURITY_ATTRIBUTES psa = 0, 
                                     DWORD stack_sz = 0)
        {
            // generic decl. for _beginthreadex() so we don't have to include 
            // process.h
            typedef unsigned (__stdcall *CrtCallback_t)(void*);
            extern unsigned long __cdecl _beginthreadex(void*, unsigned, 
                                                        CrtCallback_t, void*, 
                                                        unsigned, unsigned*);
            unsigned thread_id;
            return (HANDLE)_beginthreadex(psa, stack_sz, &CrtThreadProc, 
                                          this, flags, &thread_id);
        }//create_thread
    };//Thread<crt_thread_api>
    
    //-----------------------------------------------------------------------------
    // Nice typedefs
    typedef Thread<win_thread_api> WinThread;
    typedef Thread<crt_thread_api> CrtThread;
    
    }// namespace CP_Thread
    
    #endif //CP_THREAD_H
    
    //-----------------------------------------------------------------------------
    //-----------------------------------------------------------------------------
    //-----------------------------------------------------------------------------
    // Example code
    //-----------------------------------------------------------------------------
    #include <iostream>
    using namespace std;
    
    #include "thread.h" // the code above
    using namespace CP_Thread;
    
    struct A
    {
        DWORD foo(ThreadBase *t)
        {
            const char *msg;
            t->GetData(msg);
            cout << msg << endl;
            return 0;
        }//foo
    };//A
    
    DWORD bar(ThreadBase *t)
    {
        const char *msg;
        t->GetData(msg);
        cout << msg << endl;
        return 0;
    }//bar
    
    int main()
    {
        WinThread thread;
        thread.SetThreadEntry(bar);
    
        thread.SetData("Non member function thread");
        thread.Start();
        thread.Join();
    
        A a;
        thread.SetThreadEntry(&a, &A::foo);
    
        thread.SetData("Member function thread");
        thread.Start();
        thread.Join();
    
        cin.get();
    
        return 0;
    }//main
    Highlights of the design:
    - Functor objects use polymorphism and member function templates in order to support both funtion and member-function objects in the non-template ThreadBase class.
    - Polymorphism and template specialization used to support multiple Win32 thread creation API's. Because of the use of templates, we only have to link with the CRT if the compiler instantiates Thread<crt_thread_api>. (This model can easily be extended to support the MFC thread API using AfxBeginThread().)

    For those who would ask: Why not just use boost::thread or pthreads for Win32? The simple answer is that I'm not trying to be platform agnostic. This is a wrapper class around common Win32 API's that most Win32 capable compilers will support with a just a copy-n-paste from this thread.

    It would be an easy change to support the boost::function[N] functor object, but I like the well defined thread function interface because it adds readability to the function's intent and context of execution.

    Feel free to ask questions - on design decisions, how it works, whatever.

    [Edit-1] (Hunter2 input)
    - renamed GetThreadData()/SetThreadData() --> GetData()/SetData()
    - renamed GetExitFlag()/SetExitFlag() --> GetFlags()/SetFlags()
    - GetData()/SetData() values now set using InterlockedExchangePointer()


    gg

    Updated 12/15/04, 9:45am EST
    Last edited by Codeplug; 12-13-2004 at 12:02 AM.

  2. #2
    Carnivore ('-'v) Hunter2's Avatar
    Join Date
    May 2002
    Posts
    2,879
    Looks awesome. Gotta go now, but I'll take a closer look later
    Just one thing though:
    // just incaset T is a const pointer, const cast it first
    data = reinterpret_cast<T>(m_thread_data);
    Am I missing something? Where's the const cast?
    Just Google It. √

    (\ /)
    ( . .)
    c(")(") This is bunny. Copy and paste bunny into your signature to help him gain world domination.

  3. #3
    Registered User Codeplug's Avatar
    Join Date
    Mar 2003
    Posts
    4,981
    Oops, that was before I added the SetThreadData(const T*) specializaiton.
    Removed the comment.

    gg

  4. #4
    Carnivore ('-'v) Hunter2's Avatar
    Join Date
    May 2002
    Posts
    2,879
    Impressed by:
    -compile_assert
    -functors

    Confused by:
    assert(false); // thread shouldn't be running!
    Kill();
    Won't Kill() never be reached?

    Also, if your thread is locked in a WaitForMultipleObjects(), how can it respond to the Get/SetExitFlag()'s and accordingly exit? Or would you pass an event handle as the parameter of Get/SetExitFlag() before starting the thread, then retrieve it in the thread procedure? But then, you might as well just send it as part of the SetThreadData() package.

    Suggestions:
    -Perhaps Set/GetThreadData() could be wrapped in critical sections?
    -In the Thread<> specializations, does create_thread() really have to be declared virtual?

    Ravings:
    -Very Cool j00 = teh l33t thr34d3r.
    Just Google It. √

    (\ /)
    ( . .)
    c(")(") This is bunny. Copy and paste bunny into your signature to help him gain world domination.

  5. #5
    Registered User Codeplug's Avatar
    Join Date
    Mar 2003
    Posts
    4,981
    >> assert(false); // thread shouldn't be running!
    When NDEBUG is defined, assert() does nothing. The idea is that debug builds of the code will give you a nasty-gram if any of the assertions are false, but the release build will not.
    Handling the destruction of a thread object is really a matter of personal preference. For example, MFC's CWinThread object simply closes the thread's handle in its destructor without killing it. What I don't like about that is if you're main()/WinMain() thread exits while there are still other threads in your process, then the process lives on.
    So I decided not to allow the duration of the thread to outlive the duration of the thread object. Which makes sense to me - if the thread object dies then the thread dies too.

    >>...how can it respond to the Get/SetExitFlag()'s...
    The short answer is periodic polling.

    I struggled a bit with this change. My thinking was that creating an event object for every thread instance was a bit of an overkill. However, I wanted to provide something that could be used to signify an exit condition.
    I really meant for that interface to be GetFlags() / SetFlags() - just something generic that can be used in a thread-safe manner for whatever.

    >>...you might as well just send it as part of the SetThreadData() package.
    That's what I was think'n.

    >>Perhaps Set/GetThreadData() could be wrapped in critical sections?
    Well, my original intentions for Set/GetThreadData() was:
    - call SetThreadData() before starting the thread
    - thread then calls GetThreadData() only once to get its data

    But I like your thinking! - there's no reason for restricting it to that particular usage scenario. Making m_thread_data volatile and using InterlockedExchangePointer() is perfectly suited for the job
    I think I'm gonna settle on:
    GetData() / SetData()
    GetFlags() / SetFlags()

    >>In the Thread<> specializations, does create_thread() really have to be declared virtual?
    No, but I do it as form of self-documenting the code. Any method that overrides a virtual method is also virtual, so I explicity use the virtual keyword in these situations.

    Great suggestions! I'll get those changes edited into the post soon. And I encourage others to pipe up with gripes, suggestions, and questions.

    gg

  6. #6
    Lead Moderator kermi3's Avatar
    Join Date
    Aug 1998
    Posts
    2,595
    Original Post Updated at Codeplug's Request.

    Kermi3
    Kermi3

    If you're new to the boards, welcome and reading this will help you get started.
    Information on code tags may be found here

    - Sandlot is the highest form of sport.

Popular pages Recent additions subscribe to a feed

Similar Threads

  1. Terminating secondary thread from another thread
    By wssoh85 in forum C++ Programming
    Replies: 13
    Last Post: 12-19-2008, 05:14 AM
  2. using this as synchronization object
    By George2 in forum C# Programming
    Replies: 0
    Last Post: 03-22-2008, 07:49 AM
  3. Question on l-values.
    By Hulag in forum C++ Programming
    Replies: 6
    Last Post: 10-13-2005, 04:33 PM
  4. Thread Synchronization :: Win32 API vs. MFC
    By kuphryn in forum Windows Programming
    Replies: 2
    Last Post: 08-09-2002, 09:09 AM
  5. pthread api vs win32 thread api
    By Unregistered in forum Windows Programming
    Replies: 1
    Last Post: 11-20-2001, 08:55 AM