Home > Store > Operating Systems, Server > Microsoft Servers

Multithreading Applications in Win32: The Complete Guide to Threads

Register your product to gain access to bonus material or receive a coupon.

Multithreading Applications in Win32: The Complete Guide to Threads

Book

  • Your Price: $39.99
  • List Price: $49.99
  • Usually ships in 24 hours.

Description

  • Copyright 1997
  • Dimensions: 7-3/8x9-1/4
  • Pages: 400
  • Edition: 1st
  • Book
  • ISBN-10: 0-201-44234-5
  • ISBN-13: 978-0-201-44234-2

Using multiple threads, you can create high-performance servers, build extensions for Internet servers, take advantage of multiprocessor systems, build sophisticated objects in OLE and COM, and improve application responsiveness. Writing such software requires more than theory and a reference manual; it requires a comprehensive understanding of how everything fits together and a guide to what works and what doesn't.

Multithreading is supported under Windows NT and Windows 95 and later through the Win32 API, but coverage of this important topic has been sporadic and incomplete until now. In Multithreading Applications in Win32, with just enough theory and lots of sample code, Jim Beveridge and Bob Wiener show developers when, where, and how to use multithreading. Included in the book are:

  • Internet development examples, including ISAPI and WinSock.
  • Hands-on coverage of how to use threads and overlapped I/O for server development.
  • How to use the C run-time library and MFC in a multithreaded environment.
  • Examples in C and C++.
  • Comparisons to UNIX for developers transitioning from UNIX to Win32.

The associated web site includes the code and sample applications from the book, including code that works with Internet WinSock.



0201442345B04062001

Downloads

CD Contents

This file contains the CD Contents from the book Multithreading Applications in Win32

Sample Content

Downloadable Sample Chapter

Using C++

This chapter describes how to create multiple threads with C++ classes. It shows how C++ can make multithreaded programming significantly easier and safer.

Up to this point, all of our examples have been in C. Now we will start using C++. C++ provides significant benefits to writing multithreaded programs because you can guarantee how objects are accessed and synchronized.

I will assume you have a basic understanding of C++, including classes, virtual functions, constructors, and destructors. If you do not already understand these terms, there are hundreds of books on C++ where you can learn about them.

Handling the Buggy Prototype for _beginthreadex( )

Before I go any further, I want to point out the there is a bug in the prototype for _beginthreadex() in the C run-time library in Visual C++ 4.x. The problem does not show up in C, but C++ uses much stricter type checking. The problem stems from the prototype for _beginthreadex():

unsigned long _beginthreadex( 
                    void *security,
                    unsigned stack_size,
                    unsigned ( * start_address ) (void *), 
                    void *arglist,
                    unsigned initflag,
                    unsigned *thrdaddr );

The third and sixth parameters, start_address and thrdaddr, are both defined with unsigned, or more formally, unsigned int. If you look at the declaration for CreateThread(), these parameters are defined with DWORD, which is really unsigned long. Bit-wise, these two types are identical in a 32-bit compiler, so the C compiler ignores the difference. However, the stricter type checking for C++ in Visual C++ 4.x does notice the difference. If you declare thrdaddr to be DWORD and try to pass its address, you receive this error:

BadClass.cpp(30) : error C2664: '_beginthreadex' : cannot
convert parameter 6 from 'unsigned long *' to 'unsigned
int *' (new behavior; please see help)

The last part about "new behavior" explains why this problem did not show up in Visual C++ 2.x, which is when _beginthreadex() was introduced.

There are two workarounds for this problem. The first is to declare your variables unsigned, which is the way _beginthreadex() wants them. This solution has the advantage of simplicity, but if the prototype is ever fixed, then it would be necessary to go back and change all the variable types. This change could potentially ripple out and affect other parts of the code.

The second workaround is to declare the variables the way CreateThread() wants them and cast the variables when passing them to _beginthreadex(). I will use this method because the cast can be buried in a typedef that can easily be updated if the prototype changes.

The updated version of the skeleton for using _beginthreadex() is shown in the Listing 9-1. The type PBEGINTHREADEX_THREADFUNC provides a way to cast the thread startup function, and the type PBEGINTHREADEX_THREADID provides a cast for the thread ID. I will use these prototypes throughout the book.

Listing 9-1. CPPSKEL.CPP-Minimal thread startup code

/*
 * Cppêkel.cpp
*
 * Show how to cast the parameters to _beginthreadex
 * so tha they will work in Visual C++ 4.x with
 * the new stricter type checking.
 *
 * Build this file with the command line:
 *
*        cl /MD CppSkel.cpp
 *
 */

#include  
#include  
#include  

typedef unsigned (WINAPI  *PBEGINTHREADEX_THREADFUNC) (
       LPVOID LPTHREADPARAMETER
       );
TYPEDEF UNSIGNED *PBEGINTHREADEX_THREADID;

DWORD WINAPI ThreadFunc (LPVOIC);

int main ( )
{
     HANDLE hThread;
     DWORD threadId;
     int  I;

     hThread = (HANDLE)_beginthreadex(NULL,
                    0,
                     (PBEGINTHREADEX_THREADFUNC) ThreadFunc,
                     (LPVOID) I,
                     0,
                     (PBEGINTHREADEX_THREADID)&threadId
           );
     if  (hThread)  {
           WaitForSingleObject (hThread,  INFINITE);
            CloseHandle (hThread);
     }
     return EXIT_SUCCESS;

}

DWORD WINAPI ThreadFunc (LPVOID n)
{
     // Do something . . .
     return 0;
}

Starting a New Thread with an Object

The hidden this pointer in C++ creates some problems when trying to start a thread. Here are the wrong way and the right way to fix it.

The Wrong Way

When most people try to write their first multithreaded C++ function, they come up with the idea of starting an object in its own thread. Typically they have two member functions, one function that can be called to start the new thread and one function where the new thread begins. This example class ThreadObject is typical:

class ThreadObject
{
public:
    void StartThread();
    virtual DWORD WINAPI ThreadFunc(LPVOID param); 
private:
    HANDLE m_hThread;
    DWORD m_ThreadId;
};

ThreadObject encapsulates the thread startup and remembers information about the thread once it has started up. ThreadObject is a base class that can be subclassed by any object that needs to work in a new thread. The derived class overrides the virtual function ThreadFunc() and then gets the thread startup functionality "for free." Listing 9-2 is a short sample program that shows the class being used.

Listing 9-2. BADCLASS.CPP-How not to start a thread with an object
/*
 * BadClass.cpp
 *
 * Shows the wrong way to try to start a thread 
 * based on a class member function. 
 *
 * Build this file with the command line: 
 *
 * cl /MD BadClass.cpp
 *
 */

#include <windows.h>
#include <stdio.h>
#include <process.h>

typedef unsigned (WINAPI *PBEGINTHREADEX_THREADFUNC)( 
    LPVOID lpThreadParameter
    );
typedef unsigned *PBEGINTHREADEX_THREADID;

class ThreadObject
{
public:
    ThreadObject();
    void StartThread();
    virtual DWORD WINAPI ThreadFunc(LPVOID param); 
    void WaitForExit();
private:
    HANDLE m_hThread;
    DWORD m_ThreadId;
};

ThreadObject::ThreadObject()
{
    m_hThread = NULL;
    m_ThreadId = 0; 
}

void ThreadObject::StartThread()
{
    m_hThread = (HANDLE)_beginthreadex(NULL,
               0,
               (PBEGINTHREADEX_THREADFUNC)ThreadFunc,
               0,
               0,
               (PBEGINTHREADEX_THREADID)&m_ThreadId ); 
    if (m_hThread) { 
        printf("Thread launched\n"); 
    }
}

void ThreadObject::WaitForExit()
{
    WaitForSingleObject(m_hThread, INFINITE); 
    CloseHandle(m_hThread);
}


DWORD WINAPI ThreadObject::ThreadFunc(LPVOID param) 
{
    // Do something useful ...
    return 0;
}

void main()
{
    ThreadObject obj;

    obj.StartThread();
    obj.WaitForExit();
}

There is only one small problem with this program. It does not compile. If you try to compile it, you get this somewhat cryptic error with Visual C++ 4.1:

BadClass.cpp(48) : error C2643: illegal cast from pointer to member

The problem is even worse if you used the first workaround to the _beginthreadex bug and declared the ThreadFunc() to be unsigned instead of DWORD. In this case, you would not need the cast and the function call would look like this:

m_hThread = (HANDLE)_beginthreadex(NULL,
     0,
     ThreadFunc,
     0,
     0,
     &m_ThreadId );

This version of the program actually compiles and links successfully. (It should not, this is a bug in the compiler.) The code, correctly, crashes as soon as you run it.

The screen prints out "Thread launched," so the call to _beginthreadex() worked, but the call stack in the debugger shows these calls:

'vcall'() + 4 bytes
BaseThreadStart@8 + 97 bytes

Apparently the thread itself crashed during startup, but there is no clear reason why.

Dissecting a C++ Member Function

To understand why the code in Listing 9-1 does not work, it is necessary to take a short refresher in how member functions actually work. A non-static class member function actually has a hidden parameter that gets passed on the stack. The compiler uses it whenever it accesses a member variable or whenever you explicitly use the this pointer inside a member function. Therefore, the function ThreadObject::ThreadFunc(LPVOID param) really has two parameters, the this pointer and param.

When the operating system starts a new thread, it also creates a new stack for the new thread. The operating system must recreate the function call to your thread startup function on this new stack. That is why it is so important that the calling convention, __cdecl or WINAPI (or __stdcall, which is the same as WINAPI), matches properly.

The reason that BADCLASS crashed is that ThreadObject::WaitForExit() expected a this pointer, but the operating system only knew to push param onto the new stack. Abracadabra: instant crash.

The Right Way

To start an object with a member function, use either a static member function or a C-style function, either of which then calls the proper member function. Essentially, these techniques just introduce a helper function that correctly builds the stack for calling a member function. Internally, the two methods are equivalent, but a static member function has the added advantage of having access to the class's private and protected member data.

Listing 9-3 gives an example that uses a static member function.

Listing 9-3. MEMBER.CPP-Starting a thread with a member function

/*
 * Member.cpp
 *
 * Shows how to start a thread based on a 
 * class member function using either a static 
 * member function or a C-style function. 
 *
 * Build this file with the command line: 
 *
 * cl /MD Member.cpp
 *
 */

#include <windows.h>
#include <stdio.h>
#include <process.h>

typedef unsigned (WINAPI *PBEGINTHREADEX_THREADFUNC)( 
    LPVOID lpThreadParameter
    );
typedef unsigned *PBEGINTHREADEX_THREADID; 



class ThreadObject
{
public:
    ThreadObject();
    void StartThread();
    virtual DWORD WINAPI ThreadFunc(LPVOID param); 
    void WaitForExit();

protected:
    virtual DWORD ThreadMemberFunc();
    
    HANDLE m_hThread;
    DWORD m_ThreadId;
};

ThreadObject::ThreadObject()
{
    m_hThread = NULL;
    m_ThreadId = 0; 
}

void ThreadObject::StartThread()
{
    m_hThread = (HANDLE)_beginthreadex(NULL,
               0,
               (PBEGINTHREADEX_THREADFUNC) ThreadFunc, 
               (LPVOID) this,
               0,
               (PBEGINTHREADEX_THREADID) &m_ThreadId ); 
    if (m_hThread) { 
        printf("Thread launched\n"); 
    }
}

void ThreadObject::WaitForExit()
{
    WaitForSingleObject(m_hThread, INFINITE); 
    CloseHandle(m_hThread);
}

//
// This is a static member function. Unlike 
// C static functions, you only place the static 
// declaration on the function declaration in the 
// class, not on its implementation. 
//
// Static member functions have no "this" pointer, 
// but do have access rights. 
//
DWORD WINAPI ThreadObject::ThreadFunc(LPVOID param) 
{
    // Use the param as the address of the object 
    ThreadObject* pto = (ThreadObject*)param; 
    // Call the member function. Since we have a 
    // proper object pointer, even virtual functions 
    // will be called properly. 
    return pto->ThreadMemberFunc();
}

DWORD ThreadObject::ThreadMemberFunc()
{
    // Do something useful ... 
    return 0;
}

void main()
{
    ThreadObject obj; 

    obj.StartThread();
    obj.WaitForExit();
}

To change the code to start up with a C-style function instead of a static member function, you need only to make a few minor changes to the code. You must declare ThreadFunc() with a C-style prototype, you must move the thread startup member function into the public section of the class, and you must take the class name off the member function. The complete program is on the CD-ROM in MEMBER2.CPP. Only the changes are shown here:

DWORD WINAPI ThreadFunc(LPVOID param);

class ThreadObject
{
public:
    // . . .
    // Thread member function must be public 
    // or the C-style function will not have 
    // access rights.
    virtual DWORD ThreadMemberFunc(); 

protected:
    // . . .
};

DWORD WINAPI
ThreadFunc(LPVOID param) 
{
    ThreadObject* pto = (ThreadObject*)param; 
    return pto->ThreadMemberFunc();
}

Building Safer Critical Sections

There are so many advantages to doing multithreading with C++, when compared to C, it is hard to know where to begin talking about them. Over the next few sections, I will show how to write safer multithreaded code by relying on constructors and destructors, then show how to make the locking mechanisms interchangeable by using virtual functions and polymorphism.

Let's start by creating a simple class that embodies a critical section. We will use two of the most basic features of C++, the constructor and the destructor, which will provide guaranteed initialization and cleanup.

Remember in Chapter 3 when you always had to worry about calling functions such as InitializeCriticalSection() and DeleteCriticalSection()? By using the constructor and destructor you know these will always be called properly.

Listing 9-4 shows a very simple example.

Listing 9-4. Encapsulating a critical section with a C++ class

class CriticalSection
{
public:
    CriticalSection();
    ~CriticalSection();
    void Enter();
    void Leave();
private:
    CRITICAL_SECTION m_CritSect; 
};

CriticalSection::CriticalSection()
{
    InitializeCriticalSection(&m_CritSect);
}

CriticalSection::~CriticalSection()
{
    DeleteCriticalSection(&m_CritSect);
}

void CriticalSection::Enter()
{
    EnterCriticalSection(&m_CritSect);
}

void CriticalSection::Leave()
{
    LeaveCriticalSection(&m_CritSect);
}

At first this class seems downright boring, but it actually provides some powerful advantages to using just the straight API functions. To protect a string variable, we add a member which is a CriticalSection. Because C++ will automatically call the constructor and destructor, developers using the string class will not have to make any changes to their code to handle the critical section.

For example, Listing 9-5 shows a String class that uses the CriticalSection class.

Listing 9-5. Using CriticalSection in a String class

class String
{
public:
    String();
    virtual ~String();
    virtual void Set(char* str); 
    int GetLength();
private:
    CriticalSection m_Sync; 
    char*           m_pData;
};

String::String()
{
    // The constructor for m_Sync will have 
    // already been called automatically because 
    // it is a member variable. 
    m_pData = NULL;
}

String::~String()
{
    // Use the "array delete" operator. 
    // Note: "delete" checks for NULL automatically. 
    m_Sync.Enter();
    delete [] m_pData;
    m_Sync.Leave();
    // The destructor for m_Sync will be 
    // called automatically.
}

void String::Set(char *str) 
{
    m_Sync.Enter();
    delete [ ]m_pData;
    m_pData = new char[::strlen(str)+1]; 
    ::strcpy(m_pData, str); 
    m_Sync.Leave();
}

int String::GetLength()
{
    if (m_pData == NULL) 
        return 0;
    m_Sync.Enter();
    int len = ::strlen(m_pData);
    m_Sync.Leave();
    return len;
}

You can declare a variable of type String and use it without ever realizing that the critical section exists and that synchronization is taking place. For example, this function would now work fine in a multithreaded application:

void SomeFunction(String& str)
{
    str.Set("Multithreading");
}

Building Safer Locks

Now that we have built a critical section that automatically cleans itself up, let's create another class that automatically cleans up the lock. If you try to write a function like Truncate() with class CriticalSection as it is currently defined, you end up having to do a lot of manual cleanup. For example, in order to return from the middle of this function, you need to call Release() at each point of return. (Obviously, this particular example could be "fixed" by structuring the logic a little differently, but allow me to use the example for illustration.)

void String::Truncate(int length) 
{
    if (m_pData == NULL) 
        return 0;
    m_Sync.Enter();
    if (length >= GetLength()) 
    {
        m_Sync.Leave();
        return;
    }
    m_pData[length] = '\0'; 
    m_Sync.Leave();
}

We can create another class, Lock, whose constructor and destructor are responsible for entering and leaving the critical section (Listing 9-6). The constructor for Lock takes a CriticalSection as its only argument. Internally, Lock keeps a pointer to the CriticalSection being locked.

Listing 9-6. Abstracting synchronization objects

class Lock
{
public:
    Lock(CriticalSection* pCritSect); 
    ~Lock();
private:
    CriticalSection* m_pCritical; 
};

Lock::Lock(CriticalSection* pCritSect) 
{
    m_pCritical = pCritSect; 
    EnterCriticalSection(m_pCritical);
}

Lock::~Lock()
{
    LeaveCriticalSection(m_ pCritical); 
}

Just like class CriticalSection, class Lock does not do much by itself, but look how easy it is to rewrite Truncate() using Lock. The destructor will automatically be called and the critical section will be unlocked. It is impossible to "forget" to unlock the critical section.

void String::Truncate(int length) 
{
    if (m_pData == NULL) 
        return 0;
    // Declaring a "Lock" variable will call 
    // the constructor automatically. 
    Lock lock(&m_Sync);
    if (length >= GetLength()) 
    {
       // lock cleans itself up automatically 
       return;
    }
    m_pData[length] = '\0'; 
    // lock cleans itself up automatically 

}

In C++, when a variable goes out of scope, its destructor is called automatically. Whether the function goes out of scope because of a return in the middle of the function or because the function "falls off the end," the destructor for the variable lock will still be called.

Building Interchangeable Locks

Now we will use C++ virtual functions to build synchronization mechanisms that can be freely interchanged. In C++, an abstract base class (ABC) can be used to define an object with a standard interface. We will build an ABC called LockableObject that can be used to build concrete classes that implement particular kinds of synchronization mechanisms.

Here is LockableObject. The design is similar to the CriticalSection class, except that we will rename Enter() and Leave() to reflect the fact that this class will be used for objects other than just for critical sections.

class LockableObject
{
public:
    LockableObject() {}
    virtual ~LockableObject() {} 
    virtual void Lock() = 0; 
    virtual void Unlock() = 0; 
};

Now we will create version 2 of CriticalSection as a class derived from LockableObject (Listing 9-7). It looks almost the same, except for the declaration itself. Each of the member functions except the constructor is declared to be virtual.

Listing 9-7. A CriticalSection as a LockableObject

class CriticalSectionV2 : public LockableObject 
{
public:
    CriticalSectionV2();
    virtual ~CriticalSectionV2();
    virtual void Lock(); 
    virtual void Unlock(); 
private:
    CRITICAL_SECTION m_CritSect; 
};

CriticalSectionV2::CriticalSectionV2()
{
    InitializeCriticalSection(&m_CritSect);
}

CriticalSectionV2::~CriticalSectionV2()
{
    DeleteCriticalSection(&m_CritSect);
}

void CriticalSectionV2::Lock()
{
    EnterCriticalSection(&m_CritSect);
}

void CriticalSectionV2::Unlock()
{
    LeaveCriticalSection(&m_CritSect);
}

Now we can write version 2 of the Lock class to generically take a LockableObject instead of taking a specific kind of synchronization object. Notice how virtual functions are used to lock the object without having to know the exact type of the object (Listing 9-8).

Listing 9-8. Making locking foolproof

class LockV2
{
public:
    LockV2(LockableObject* pLockable); 
    ~LockV2();
private:
    LockableObject* m_pLockable; 
};

LockV2::LockV2(LockableObject* pLockable) 
{
    m_pLockable = pLockable; 
    m_pLockable->Lock();
}

LockV2::~LockV2()
{
    m_pLockable->Unlock();
}

We now have the classes CriticalSectionV2 and LockV2, which work in concert with each other. By using the C++ classes instead of directly calling the Win32 API, it is possible to guarantee that initialization and cleanup are always done and will be performed correctly. Now let's write version 2 of the String class to use these new synchronization classes (Listing 9-9).

Listing 9-9. Rewriting the String class with foolproof locking

class StringV2
{
public:
    StringV2();
    virtual ~StringV2();
    virtual void Set(char *str); 
    int GetLength();
private:
    CriticalSectionV2 m_Lockable; 
    char*             m_pData;
};

StringV2::StringV2()
{
    m_pData = NULL;
}

StringV2::~StringV2()
{
    // The program must ensure that 
    // it is safe to destroy the object. 
    delete [ ] m_pData;
}

void StringV2::Set(char *str) 
{
    LockV2 localLock(&m_Lockable);
    delete [ ] m_pData;
    m_pData = NULL;
    // In case new throws an exception 
    m_pData = new char[::strlen(str)+1]; 
    ::strcpy(m_pData, str); 
}

int StringV2::GetLength()
{
    LockV2 localLock(&m_Lockable);
    if (m_pData == NULL) 
        return 0;
    return ::strlen(m_pData);
}

Handling Exceptions A final tremendous advantage to implementing synchronization mechanisms using classes is that they will work with exception handling. For this section, I will assume you are already familiar with how Win32 exceptions and C++ exceptions work.

The advantage to exception handling is that it works outside of the standard function call/return value paradigm. When an exception is thrown, Win32 works in combination with the C++ run-time library to unwind the stack and clean up all variables. Writing code that is safe for exception handling is substantially more difficult in C because you have to do all the bookkeeping yourself. In C++, with a little bit of foresight, you can let the compiler do all of the work.

By using classes like the Lock class we developed in this chapter, locked objects will automatically be destructed as the stack unwinds. The destructor will unlock the object. The net result is that locks will be cleaned up automatically.

Summary

As you have seen throughout this book, one of the keys to success in multithreading is absolute diligence in handling your data. Do not ever touch data that you cannot guarantee to be in a consistent state.

The true beauty of C++ is that you can use the class definition to absolutely guarantee that the data is valid. By confining the data to only the private portion of the class definition, you can force users to only access the data with well-defined mechanisms in the public member functions. Although it can easily be argued that all of these things can be done in C, there is substantial benefit to having the compiler enforce your design needs. It takes just one developer on a team to create an obscure bug by deciding to "take a shortcut" by accessing data directly without a lock.

Updates

Submit Errata

More Information

Unlimited one-month access with your purchase
Free Safari Membership