Win32 Multithreading Made Easy

Multithreading logic is hard to write and hard to maintain. So keep it simple and separate.


August 01, 1999
URL:http://www.drdobbs.com/win32-multithreading-made-easy/184403684

August 1999/Win32 Multithreading Made Easy/Figure 1

Figure 1: An example application

/* =======================================================
Main.cpp
======================================================= */

#include "derived.h"
#include "multithread.h"
#include <iostream>


int main()
{
    using namespace std;
    const int nSize = 26;
    int loop;

    // Create threadable objects

    Derived ObjectArray[nSize];
    for (loop = 0; loop < nSize; ++loop)
    {
        // letters of the alphabet
        ObjectArray[loop].SetChar('a' + (loop % 26) );
    }

    // Create threads

    MultiThread ThreadArray[nSize];
    for (loop = 0; loop < nSize; ++loop)
    {
        ThreadArray[loop].Initialize( &ObjectArray[loop], 10 );
        ThreadArray[loop].Run();       
    }

    // Wait until they finish

    for (loop = 0; loop < nSize; ++loop)
    {
        ThreadArray[loop].WaitUntilDone();
    }

    cout << endl;
    return 0;
}

August 1999/Win32 Multithreading Made Easy/Figure 2

Figure 2: Class Derived interface and implementation

/* ==========================================
Derived.h
========================================== */

#ifndef DERIVED_H
#define DERIVED_H

#ifndef THREADABLEOBJECT_H
#include "threadableobject.h"
#endif

class Derived : public ThreadableObject
{
public:
    Derived();
    ~Derived();
    
    void SetChar( char ch );

    // Pure Virtual function in MultiThread
    virtual bool 
    ThreadableTask(DWORD* dwReturnCode);

private:
    char m_ch;
    int m_nCount;
};
#endif

/* ==========================================
Derived.cpp
========================================== */

#include "derived.h"
#include <iostream>


Derived::Derived()
:ThreadableObject()
{
    m_ch = '\0';
    m_nCount = 0;
}


Derived::~Derived()
{}


void Derived::SetChar(char ch)
{
    m_ch = ch;
}


bool 
Derived::ThreadableTask( DWORD* dwReturnCode )
{
    using namespace std;

    while ( m_nCount < 100 )
    {
        cout << m_ch;
        m_nCount++;
        // return true;
    }

    *dwReturnCode = 0;
    return false;
}


August 1999/Win32 Multithreading Made Easy/Figure 3

Figure 3: Classes MultiThreadState and MultiThread

/* ============================================================
MultiThread.h
============================================================ */

#ifndef MULTITHREAD_H
#define MULTITHREAD_H

#include <windows.h>

class ThreadableObject;

DWORD WINAPI MultiThreadImpl( LPVOID lpParam );


class MultiThreadState 
{
public:
    enum {  UNINITIALIZED, INITIALIZED, 
            WORKING, SLEEPING, DONE,
            SUSPENDED, RESUMED };

    MultiThreadState();
    ~MultiThreadState();
    void Reset();

private:
    // not implemented
    MultiThreadState( const MultiThreadState& copyMe );
    MultiThreadState& operator=( const MultiThreadState& rhs );

    friend class MultiThread;
    friend DWORD WINAPI MultiThreadImpl( LPVOID lpParam );

    // Closing handle twice is an error.
    void CloseThreadHandle();
    HANDLE  m_hThread;

    HANDLE  m_hRunEvent;
    HANDLE  m_hWorkEvent;
    HANDLE  m_hDoneEvent;

    // Controls access to following data.
    HANDLE  m_hMutex;
    DWORD   m_dwCycleTime;
    bool    m_bStopThread;
    int     m_eState;
    DWORD   m_dwSuspendCount;

    ThreadableObject* m_pObject;
};


class MultiThread
{
public:
    MultiThread();
    ~MultiThread();

    bool Initialize(
                ThreadableObject* ptrClass,
                DWORD dwMilliSecs,
                DWORD dwStackSize = 0 );

    void Run();

    void RequestStop();
    bool IsStopping() const;

    void DoWork();

    DWORD CycleTime() const;
    void CycleTime( DWORD dwCycleTime );

    // Similar to Win32 functions, except they return the
    // threads current suspend count, not the previous count.

    DWORD Suspend();
    DWORD Resume();
    bool IsSuspended( DWORD* dwSuspendCount = NULL ) const;

    // Same as Win32 functions GetExitCodeThread and 
    // SetThreadPriority.

    BOOL ExitCode( LPDWORD lpExitCode );

    BOOL Priority( int nPriority );

    // Suspends caller until DoneEvent is signalled.

    void WaitUntilDone();

    // See enum in MultiThreadState for return values.

    int ThreadState() const;

private:
    // not implemented
    MultiThread( const MultiThread& copyMe );
    MultiThread& operator=( const MultiThread& rhs );

    MultiThreadState m_td;
};
#endif

/* ==============================================
MultiThread.cpp
============================================== */

#include "multithread.h"
#include "threadableobject.h"
#include "mutexlock.h"

#include <assert.h>

// Error code for SuspendThread, ResumeThread.
static const DWORD SR_ERROR = 0xFFFFFFFF;


DWORD WINAPI MultiThreadImpl(LPVOID lpParam)
{
    MultiThreadState* ptrState = (MultiThreadState*) lpParam;

    // Synch on the Run Event
    WaitForSingleObject( ptrState->m_hRunEvent, INFINITE );

    bool bStopThread = false;
    bool bWorkInProgress = true;

    DWORD dwCycleTime = 0;
    DWORD dwReturn = 0;
    ThreadableObject *pObj = 0;

    // Latch values once to start loop
    {
        MutexLock( ptrState->m_hMutex );
        dwCycleTime = ptrState->m_dwCycleTime;
        ptrState->m_eState = MultiThreadState::SLEEPING;
        pObj = ptrState->m_pObject;
    }

    do
    {
        // Wait for a Work event, or a timeout.
        WaitForSingleObject( ptrState->m_hWorkEvent, 
            dwCycleTime );

        { 
            MutexLock( ptrState->m_hMutex );
            ptrState->m_eState = MultiThreadState::WORKING;
        }

        bWorkInProgress = pObj->ThreadableTask( &dwReturn );

        if ( bWorkInProgress )
        {
            // Latch values for next repitition.
            MutexLock( ptrState->m_hMutex );
            dwCycleTime = ptrState->m_dwCycleTime;
            bStopThread = ptrState->m_bStopThread;
            ptrState->m_eState = MultiThreadState::SLEEPING;
        }
    } while ( !bStopThread && bWorkInProgress );

    // Thread finished.
    {
        MutexLock( ptrState->m_hMutex );
        ptrState->m_bStopThread = true;
        ptrState->m_eState = MultiThreadState::DONE;
    }

    SetEvent(ptrState->m_hDoneEvent);
    return dwReturn;
}


MultiThreadState::MultiThreadState()
{
    m_hThread = 0;

    // lpEventAttributes, bManualReset, bInitialState, lpName
    m_hRunEvent  = CreateEvent( NULL, false, false, NULL );
    assert( m_hRunEvent );

    m_hWorkEvent = CreateEvent( NULL, false, false, NULL );
    assert( m_hWorkEvent );

    // Manual reset since multiple threads can wait on this 
    // event.
    m_hDoneEvent = CreateEvent( NULL, true,  false, NULL );
    assert( m_hDoneEvent );

    // Security Attr., bInitialOwner, lpName
    m_hMutex = CreateMutex( NULL, false, NULL );
    assert( m_hMutex );

    // Should be consistent with code in Reset.
    m_dwCycleTime = 0;
    m_bStopThread = false;
    m_eState = UNINITIALIZED;
    m_dwSuspendCount = 0;
    m_pObject = 0;
}



MultiThreadState::~MultiThreadState()
{
    CloseThreadHandle();

    CloseHandle( m_hRunEvent );
    CloseHandle( m_hWorkEvent );
    CloseHandle( m_hDoneEvent );
    CloseHandle( m_hMutex );
}


void MultiThreadState::Reset()
{
    CloseThreadHandle();

    // This code should be consistent with
    // code in the constructor.

    ResetEvent( m_hRunEvent );
    ResetEvent( m_hWorkEvent );
    ResetEvent( m_hDoneEvent );

    m_dwCycleTime = 0;
    m_bStopThread = false;
    m_eState = UNINITIALIZED;
    m_dwSuspendCount = 0;
    m_pObject = 0;    
}


void MultiThreadState::CloseThreadHandle()
{
    // Closing a HANDLE twice is an error.
    // Allowing multiple calls to Initialize
    // requires this.
    if ( m_hThread != 0 )
    {
        CloseHandle( m_hThread );
        m_hThread = 0;
    }
}


MultiThread::MultiThread()
:m_td( MultiThreadState() )
{}


MultiThread::~MultiThread()
{
    // Do what we can to end thread, 
    // but ThreadableTask must still cooperate!

    if ( m_td.m_hThread != 0 )
    {
        while ( IsSuspended() )
        {
            Resume();
        }

        Run();
        DoWork();

        RequestStop();
        WaitUntilDone();
    }
}


bool MultiThread::Initialize
(
    ThreadableObject* ptrClass,
    DWORD dwMilliSecs,
    DWORD dwStackSize
)
{
    MutexLock( m_td.m_hMutex );

    // User must cleanly exit previous thread, if any.
    assert( m_td.m_eState == MultiThreadState::DONE ||
            m_td.m_eState == MultiThreadState::UNINITIALIZED );
            
    bool bRetVal = false;

    // Initialize can be called multiple times.
    m_td.Reset();

    if ( m_td.m_hThread == 0 )
    {
        DWORD dwThreadId = 0;
        m_td.m_hThread = CreateThread( NULL, dwStackSize, 
                             &MultiThreadImpl, (LPVOID) &m_td,
                             0, &dwThreadId);
        if ( m_td.m_hThread )
        {
            m_td.m_dwCycleTime = dwMilliSecs;
            m_td.m_eState = MultiThreadState::INITIALIZED;
            m_td.m_pObject = ptrClass;
            bRetVal = true;
        } else 
        {
            // Explicitly set just in case.
            m_td.m_hThread = 0;
        }
    }
    return bRetVal;
}


void MultiThread::Run()
{
    MutexLock( m_td.m_hMutex );
    if ( m_td.m_eState == MultiThreadState::INITIALIZED )
    {
        SetEvent( m_td.m_hRunEvent );
    }
}


void MultiThread::RequestStop()
{
    MutexLock( m_td.m_hMutex );
    m_td.m_bStopThread = true;
}


bool MultiThread::IsStopping() const
{
    MutexLock( m_td.m_hMutex );
    return m_td.m_bStopThread;
}


void MultiThread::DoWork()
{
    MutexLock( m_td.m_hMutex );
    if ( m_td.m_eState == MultiThreadState::SLEEPING )
    {
        SetEvent( m_td.m_hWorkEvent );
    }
}


DWORD MultiThread::CycleTime() const
{
    MutexLock( m_td.m_hMutex );
    return m_td.m_dwCycleTime;
}


void MultiThread::CycleTime( DWORD dwCycleTime )
{
    MutexLock( m_td.m_hMutex );
    m_td.m_dwCycleTime = dwCycleTime;
}


DWORD MultiThread::Suspend()
{
    MutexLock( m_td.m_hMutex );
    if ( ::SuspendThread( m_td.m_hThread ) != SR_ERROR )
    {
        ++m_td.m_dwSuspendCount;
        if ( m_td.m_dwSuspendCount > 0 ) 
        {
            m_td.m_eState = MultiThreadState::SUSPENDED;
        }
    }
    return m_td.m_dwSuspendCount;
}


DWORD MultiThread::Resume()
{
    MutexLock( m_td.m_hMutex );
    if ( ::ResumeThread( m_td.m_hThread ) != SR_ERROR )
    {
        --m_td.m_dwSuspendCount;
        if ( m_td.m_dwSuspendCount == 0 )
        {
            m_td.m_eState = MultiThreadState::RESUMED;
        }
    }
    return m_td.m_dwSuspendCount;
}


bool MultiThread::IsSuspended( DWORD* dwSuspendCount ) const
{
    MutexLock( m_td.m_hMutex );
    if ( dwSuspendCount != NULL )
    {
        *dwSuspendCount = m_td.m_dwSuspendCount;
    }
    return ( m_td.m_dwSuspendCount != 0 ? true : false );
}


BOOL MultiThread::ExitCode( LPDWORD lpExitCode )
{
    return ::GetExitCodeThread(m_td.m_hThread, lpExitCode);
}


BOOL MultiThread::Priority( int nPriority )
{
    return ::SetThreadPriority( m_td.m_hThread, nPriority );
}


void MultiThread::WaitUntilDone()
{
    WaitForSingleObject( m_td.m_hDoneEvent, INFINITE );
}


int MultiThread::ThreadState() const
{
    MutexLock( m_td.m_hMutex );
    return m_td.m_eState;
}

August 1999/Win32 Multithreading Made Easy

Win32 Multithreading Made Easy

John Harrington

Multithreading logic is hard to write and hard to maintain. So keep it simple and separate.


Introduction

This article describes a multithreading framework suitable for C++ Win32 programming. The code does not require MFC (Microsoft Foundation Classes) to run. It is written directly to the Win32 API. The code is based on an article by Russell Weisz, titled "First Aid for the Thread-Impaired: Using Multiple Threads with MFC." [1]

A multithreading base class is described in Weisz's article. The implementation requires MFC. I was working on a heavily threaded application that made using Weisz's implementation difficult because my application was not MFC based. I knew it was possible to use MFC from a console interface [2], but that did not appear to be an easy solution. Using MFC would require too many modifications. I thought a better approach was to take the idea presented by Weisz and port it to the Win32 API.

The Framework

The framework consists of three classes broken out into two header files, ThreadableObject.h and MultiThread.h.

ThreadableObject is a lightweight base class that ties user code to the framework. Use of ThreadableObject keeps user objects small and draws a clean distinction between application code and the threading framework.

Multithread.h defines two classes, MultiThread and MultiThreadState, and global function MultiThreadImpl. Users interact with the thread through class MultiThread. MultiThreadState and MultiThreadImpl are declared at file scope only out of necessity. They are implementation details of the framework.

Creating a separate thread is a two-step process. First, derive your class from ThreadableObject and provide an implementation for the pure virtual function ThreadableTask. Second, create a MultiThread object and pass it a pointer to your derived class.

Once the thread is created MultiThreadState acts as repository for the thread's status. Users change the repository by making function calls on class MultiThread. The thread function, MultiThreadImpl, periodically retrieves information from the repository and responds accordingly.

An Example

I've created an example to demonstrate the framework. I describe the code in reverse order, from what it looks like on the outside to how it is implemented.

Figure 1 shows a multithreaded application. Class Derived inherits from the multithreading base class. The first loop creates nSize number of objects, in this case one for each letter of the alphabet.

The second loop creates a thread for each object and starts it. The Initialize method requires two parameters, a pointer to the ThreadableObject and the thread's cycle time, given in milliseconds. The cycle time is approximately equal to the time between successive calls to the ThreadableTask function. The thread model is described in more detail below.

There are two other parameters that could be passed to Initialize. They are the thread's stack size and a security attributes parameter. I implemented the stack size as an optional parameter. I choose not to implement a security attributes parameter because the C++ code provides access control for me. Outside objects do not have direct access to the thread's state. Some applications may require this parameter. For those that do, it can be passed through the Initialize method and to the underlying call to CreateThread.

The last loop prevents the program from exiting until all threads have completed. This code was originally required, but statements added later to MultiThread's destructor make it optional. I've left the code in for clarity.

Figure 2 shows the code for class Derived. It inherits from ThreadableObject and implements ThreadableTask, the one virtual function it declares. In this case, the function prints a character to cout. The function terminates when 100 characters have been printed. Variable dwReturnCode becomes the thread's exit code and can be set before returning from ThreadableTask for the last time. See the section "The ThreadableTask Function'' for more details.

Figure 3 shows the code for classes MultiThreadState and MultiThread. MultiThreadState encapsulates the data for a thread. It has a limited public interface but declares MultiThread and MultiThreadImpl as friends so they can conveniently access it. Users of class MultiThread do not need to know about MultiThreadState. It is declared at file scope so MulitThreadImpl can use it. See MultiThread::Initialize.

Run the example and you will see the output looks something like

aaaabbbccccccddeeeeee....

Each thread runs until it is preempted by the operating system. The number of successive letters that are identical is arbitrary. For a more even distribution of letters, uncomment the second return statement in Derived::ThreadableTask and run the example again.

The Threading Model

The thread model is summarized by the enumeration that appears in MultiThreadState. A thread created by this framework can be in one of the following states: UNINITIALIZED, INITIALIZED, WORKING, SLEEPING, DONE, SUSPENDED, and RESUMED.

MultiThread objects are UNINITIALIZED immediately after construction. The Initialize method creates the thread, changes its state to INITIALIZED and suspends it until the user calls Run. At this point, CreateThread has been called, but the first thing the thread does is wait for an object that is signaled when the user calls Run.

Once Run is called, the thread state changes to SLEEPING. It remains there for at least dwCyleTime milliseconds. If your thread must start working immediately, follow the call to Run with a call to DoWork. This will signal the event the thread requires to continue.

The thread state alternates between SLEEPING and WORKING (approximately) every dwCycleTime milliseconds until the ThreadableTask function returns false. When this occurs the thread, and the task to perform, are finished. The thread's state is changed to DONE.

Two additional states listed in the enumeration are SUSPENDED and RESUMED. They are implemented in terms of the Win32 functions SuspendThread and ResumeThread. In this implementation, I return the thread's current suspend count instead of the pervious suspend count. In addition, a resumed thread will stay in the RESUMED state until the next transition occurs, however long that may be. The thread stays RESUMED for the remainder of the state it was suspended in, whatever state that may be. The Win32 documentation lists other thread function calls that can be implemented (or changed) in a similar fashion.

The ThreadableTask Function

ThreadableTask represents the work to be done. ThreadableTask's pure virtual function is what brings the framework together. MultiThreadImpl calls this function through a ThreadableObject pointer, so the correct function is always called. Since ThreadableTask is implemented in the derived classes, the code can access all of the derived classes member functions and data. In effect, you take the work you want done and whatever data it requires, wrap it up in an object, and execute it inside another thread.

ThreadableTask takes a DWORD pointer as a parameter and returns a bool. The function returns true while there is work to do, and false when the task is completed. The parameter becomes the thread's exit code. Its use is optional. If no return code is given, the default value is zero.

The most important thing to keep in mind when implementing ThreadableTask is, "How responsive to the outside world do I want my thread to be?'' In the simplest cases, a thread runs "unattended'' until it is finished. In practice, this rarely happens. At a minimum, someone will want to cancel a thread if it is doing work that is no longer necessary.

Threads created with this framework can respond only to certain events, such as cancellations and changes in cycle time, between transitions from WORKING to SLEEPING. This is the point where MultiThreadImpl updates its state information. The more calls made to ThreadableTask, the more in tune your thread will be with the outside world.

The original example was not responsive at all. Each thread made one call to ThreadableTask. It was up to the operating system to preempt a thread and schedule another one for CPU time. The modified example was extremely responsive. It could adapt to new state information after each letter was printed. The level of granularity is entirely up to you.

Two other points to keep in mind:

1. ThreadableTask will always be called at least once. This is true even if you issue a Run command and immediately follow it with a RequestStop statement.

2. ThreadableTask must return at least once. Code in MultiThread's destructor explicitly waits for the thread to finish.

Stopping a Thread

Stopping a thread in this framework is a joint venture. RequestStop simply sets a flag inside MultiThreadState. The thread will not act on this information until there is a transition from WORKING to SLEEPING. The frequency and distribution of these transitions is controlled by the structure of ThreadableTask.

Synchronization

The execution of other threads can be postponed until a given thread terminates by calling WaitUntilDone. Each thread has an event that is signaled when the thread terminates. The event is manually reset so multiple threads can wait on a single thread to finish. Be careful to avoid situations where a MultiThread object is destroyed before all of the threads that are waiting on it can be notified. Make sure the MultiThread object stays in scope long enough to activate all the waiting threads.

Each thread in the example was an independent entity. There was no interaction between or among threads. In practice this is rare. Synchronization code must be added to either the derived class or the ThreadableTask function to coordinate the activity of cooperating threads.

Other Issues

One class that is used but not shown in the listings is MutexLock. This class is a simple wrapper for a mutex handle [3]. The timeout on the WaitForSingleObject is INFINITE.

MultiThread::Initialize uses the Win32 function CreateThread. I could instead have used functions BeginThread or BeginThreadEx. An advantage of these functions over CreateThread is that they are less prone to memory leaks when certain C runtime functions are used. My next version of this code will probably use one these functions.

While I am discussing future versions, I want to point out the biggest advantage of this framework. It separates the task you want done from the threading code that calls it. MultiThreadImpl represents one possible thread model. You could easily create another model for a specific situation and tie a ThreadableObject derived class to it.

Conclusion

This article described an object-oriented framework for Win32 that encapsulates threads. The code was based on an article presenting a similar framework for MFC. The framework cleanly separates thread specific code from the task the thread is performing.

References

[1] Russell Weisz. "First Aid for the Thread-Impaired: Using Multiple Threads with MFC." Microsoft Systems Journal, December 1996.

[2] Mike Blaszczak. Professional MFC with Visual C++ 5 (Wrox Press, 1997).

[3] Bjarne Stroustrup. Design and Evolution of C++ (Addison-Wesley, 1994), Section 8.3.2.2

John Harrington has a B.S.E.E. from MIT and an M.S. in Computer Engineering from the UCSD. He works for Nuera Communications in San Diego, a provider of high-quality packet voice communications systems and technology for voice/fax/data/video networking over IP, frame relay, and circuit-switched networks. He can be reached at [email protected].

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.