Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

A C++ Framework for DCE Threads


SP95: A C++ Framework for DCE Threads

Michael is the founder of Y Technology Inc., a consulting company that serves New York's financial district. He can be reached on CompuServe at 76367,3040.


If you've done any serious programming under UNIX, you probably appreciate its multitasking capabilities and may want to add such capabilities to your application. Using threads to multitask pieces of your program, you can divide and conquer a problem, while improving a program's throughput. Threads are often used to build servers that handle multiple clients; their real-time nature also makes them effective for simulations and real-time data delivery.

The OSF Distributed Computing Environment (DCE) supports a powerful, multithreaded programming facility through a set of API calls sometimes called "pthreads." In a recent project, I created a C++ framework under HP-UX to cope with multithreaded programming using the DCE facilities. My pthread framework (PTF) consists of five classes and one template; see Table 1. PTF makes no effort to encapsulate the entire DCE pthread API. For example, PTF creates only threads of equal priorities with round-robin scheduling. However, the framework can be extended to accommodate varied priorities and different schedules. (For more information on OSF DCE, see "Distributed Computing and the OSF/DCE," by John Bloomer, DDJ, February 1995.)

pthreads Explained

DCE threads is a facility that supports multithreaded programming. On HP-UX, it is a set of nonkernel libraries to which you link your programs. The API consists of some 50-plus calls. It is based on a POSIX standard, which is why DCE threads are sometimes referred to as "pthreads."

Much as UNIX can multitask several processes, threads allow a program to multitask pieces of itself. Unlike processes, however, threads share the same address space and can communicate with one another without complex interprocess communications. Because threads are intraprocess, they can "see" the other's static variables while maintaining private automatic variables of their own. This is possible because every thread gets its own stack.

Threads allow your program to divide and conquer a problem. For example, one thread can calculate the area under a curve between 0 and 1 while another thread does the same between 0 and -1. When the two threads complete their respective calculations, they join up and total their results for the area under the curve between -1 and +1. While nonthreaded algorithms exist to solve for an area under a curve, a threaded solution can transparently take advantage of a platform with multiple CPUs.

Threads can be used to improve a program's throughput. At one client site, we've designed a multithreaded server to support up to 100 users concurrently. One thread manages connections and dynamically creates threads to service users, thus providing one thread per user. Helper threads within the server monitor connections, message statistics, and the general health of the server. A nonthreaded solution could not handle the same number of users as cleanly; see the accompanying text box entitled "Unraveling DCE pthreads" for details.

Constructing Threads

To create a thread under DCE, you call pthread_create(), which accepts a pointer to a function as one of its arguments. This function becomes the entry point of your thread. To create a thread using PTF, on the other hand, you must derive from the abstract base class, PTObject; instead of using a pointer to a function, you supply code for the virtual function runPThread(). This has three advantages. First, a thread as an object can store related data such as the thread ID, the thread handle, mutexes, condition variables, and any data that might be shared with other threads. Secondly, pointers to functions in C are natural candidates for virtual functions in C++. Third, deriving from PTObject gives you the start(), tid(), and join() member functions in addition to the constructor and destructor.

The start() member function starts the thread object running. A thread can have four states: running, ready to run, waiting, and terminated. A running thread is one that is executing CPU instructions. Don't assume that a newly created thread starts in the running state; that will depend on the thread priority and scheduling policy, as well as CPU availability. Ideally, the constructor should start the thread, but if a CPU were available during thread creation, the thread could start running even before the constructor had completed! To prevent this, the thread object must be instantiated first, then followed with a call to start(). However, start() has its limitations. All it does is invoke DCE's pthread_create(). It cannot truly force a thread to start--that remains the responsibility of the scheduler. Still, start() gives the constructor a chance to complete its initializations and thus prevents the premature starting of a thread. Because start() invokes pthread_create(), you should not call start() more than once per PTObject.

You might also be tempted to modify start() to accept arguments. As an example, for a thread to service a particular socket, it may seem desirable to pass the file descriptor into the start() member. However, it's better to avoid overloading start(). Instead, when you derive a class from PTObject, write your constructor to accept a file descriptor and store the value in your class (perhaps in the private section). The member function tid() returns the thread ID as a unique integer. I assign the ID during thread creation using the undocumented, nonportable DCE call, pthread_getunique_np(). If you're uncomfortable using nonportable calls, modify the class to generate and assign your own unique numbers. The idea behind tid() is to manage threads based on ID and to identify threads in debugging statements.

The join() function, a wrapper for DCE's pthread_join(), allows the current thread to wait for the completion of another thread. In terms of DCE calls, pthread_create() and pthread_join() are opposites: pthread_create() splits single-threaded code for parallel processing, and pthread_join() serializes any parallel code. If the current thread calls pthread_join() on itself, the thread will deadlock. PTF's join(), however, checks for this condition. Instead of deadlocking, the thread will simply continue.

Once a thread has completed running, it is in a terminated state. Normally, you would free resources by calling C++'s delete on your thread object. You can also have a thread self-destruct by coding a delete at the end of the runPThread() routine. Having a thread invoke its own destructor avoids storing and deleting the pointers to your thread objects. Finally, you can terminate running threads by just "deleting" the thread object. I don't recommend this, however, because your thread might be in a critical state. To interrupt your thread, set a flag in your thread object to mark it for termination. The thread itself can then poll this flag and self-destruct when it is not critical.

Good Class Habits

Good, safe, robust frameworks require other member functions in PTObject and all the PTF classes: a copy constructor, an assignment operator, isValid(), and className(). For simplicity and clarity, I have omitted the copy constructor and assignment operators from the framework, so the compiler supplies its own. This will probably work, assuming the class doesn't contain pointers. PTObject doesn't contain pointers today, but may in the future. Thus, to prevent a class user from accidentally copying or assigning a thread object, I've declared those members in the header file (see Listing One), but haven't defined them in the .cpp file (Listing Two). Should the class user try to copy or assign a thread object, an error would occur at link time.

The isValid() function determines if the object was constructed properly. Recall that constructors do not return a value. Although isValid() usually acts like a bool, returning 1 or 0 for True or False, respectively, I've declared it to return an integer. I could have implemented a Boolean type, but there are endless variations that might clash with libraries from different vendors. Fortunately, a C++ bool type using "true" and "false" literals has been accepted by the ANSI/ISO committee. It's just a matter of time before compiler vendors implement the bool type, but until then, I prefer returning an integer. Checking for a bad constructor therefore involves ensuring that the object pointer is non-null and then invoking the isValid() member: 1 means the constructor completed successfully, and 0 indicates failure.

Some favor exception handling for detecting construction problems, and in fact, exception handling is the preferred method in C++. However, I've used flags for simplicity, and because HP exception handling doesn't work properly with threads as of this writing. className() returns the class name like so: const char *className() { return "PTObject" }. A typical use for this function might be to display the class name in a title bar or a message log.

Thread Synchronization

Two classes, PTMutex and PTCondVar, manage thread synchronization. DCE mutexes come in three flavors: fast, recursive, and nonrecursive. A fast mutex is synchronous: A thread will wait indefinitely until it gets the lock. This is the most commonly used type of mutex. A recursive mutex allows a thread that has already locked a mutex to lock it again (it doesn't block itself). Since the mutex is recursive, it will take as many unlocks as there were locks to clear the mutex. A nonrecursive mutex, like a fast mutex, can only be locked once, but unlike a fast mutex, if the thread tries to lock it again, it will receive an error instead of waiting indefinitely.

PTF provides the PTMutex class to encapsulate the DCE fast mutex. The constructor creates the mutex using DCE's pthread_mutex_init(), and the destructor deletes the mutex using pthread_ mutex_destroy(). Member functions lock(), unlock(), and trylock() wrap the DCE functions pthread_lock(), pthread_ unlock(), and pthread_trylock(), all returning the same respective error codes. Admittedly, PTMutex does not add much to DCE's mutex functionality, but it is important because it serves as a base class for PTCondVar. I've derived PTCondVar from PTMutex because a condition variable "is a kind of" mutex. Whereas a mutex synchronizes threads with data, a condition variable synchronizes threads with other threads. For a condition variable to be useful, it must also be paired with a mutex. A condition variable has Boolean properties and acts much like a traffic light, controlling whether the waiting thread(s) should go or continue to wait. The state of the condition variable is controlled by other threads with the help of the PTCondVar class members signal(), broadcast(), and timedWait().

The signal() function allows one thread to notify a waiting thread to continue. Threads that have a producer/consumer relationship often use this mechanism. In such a relationship, a producer thread would generate data while a consumer thread would process the data. When data is available, the producer thread would signal() the waiting thread, telling it that there is data to process. The broadcast() function is similar to signal(), except that signal() wakes the first waiting thread, and broadcast() wakes them all.

A consumer thread waits for a signal or broadcast by calling the member timedWait(). A waiting thread will resume execution only when it receives a signal or broadcast, or when a specified number of seconds have elapsed, whichever comes first. I've written timedWait() to accept an integer representing the number of seconds to wait; passing in a zero will cause the thread to wait indefinitely for a signal or broadcast.

Thread-Safe Objects

PTMutex and PTCondVar, while handy, are still primitive. An entirely thread-safe class would be more convenient. For a class to be thread safe, operations on data must be atomic: Another thread must not be able to interrupt the current one during an update operation. A thread-safe object can be achieved with the classes PTSafeObject and PTLock. Derive your class from PTSafeObject. Then, for every member function that reads or writes data, instantiate PTLock on the stack, as in Figure 1. Upon leaving the function, PTLock will automatically unlock and delete itself. If you design your application to access data through one class, this mechanism will ensure safe updates across multiple threads. Creating a mutex on the stack also prevents you from accidentally leaving the mutex in a locked state when returning early from a function (to handle an error).

PTSafeObject and PTLock operate as a pair. Actually, PTLock is nested within PTSafeObject and is also a friend of PTSafeObject. When you derive from PTSafeObject, your class inherits a mutex that PTLock locks in its constructor and unlocks in its destructor. When you delete your object, the mutex also gets deleted. This allows only one mutex per PTSafeObject, thus precluding designs that dedicate a mutex for reading and another mutex for writing.

A natural extension to PTSafeObject is to create a template version: PTTSafeType (the extra "T" denotes template); see Listings Three and Four. A template allows built-in types, such as an int, to be thread safe. I found this especially useful for managing sequence numbers, because several threads could simultaneously update the sequence number. The template provides thread-safe get() and set() functions and overloads the prefix and postfix increment and decrement operators, making them thread safe as well; see Figure 2.

Wrapping Up

To test the thread framework, a sample program (available electronically; see "Availability" on page 3) creates a "boss" thread, which in turn creates ten "worker" threads. The pointers to the worker threads are stored in an array so that they can be deleted later. The worker threads wait on a condition variable for either a broadcast() from the boss thread or for 15 seconds to elapse, whichever comes first. To make the program a little more interesting, the boss thread issues a signal() rather than a broadcast(), thus waking up only one worker thread. The remaining worker threads sleep for 15 seconds before starting. All worker threads print "Hello World" to stdout along with their thread IDs. The worker threads then join the boss thread and are deleted.

Under HP-UX, I compiled PTF with the following definitions: _REENTRANT, _POSIX_SOURCE, and _CMA_NOWRAPPERS. I had to use _REENTRANT because threaded code must be reentrant, and I opted for _POSIX_SOURCE because pthreads are based on a POSIX standard. As for _CMA_NOWRAPPERS, the threads package OSF provides to vendors was originally called CMA. HP offers CMA compatibility by providing wrappers in header files that redefine standard library calls with calls to cma_ routines. For example, read() is replaced with cma_read(). Unfortunately, enabling CMA wrappers can lead to name collisions, especially in C++, where classes commonly have a member function named read(). When linking the test program, you'll need the following two libraries (shared or archive): libdce and libc_r. libdce provides DCE support. libc_r is HP's thread-safe library of system calls.

Finally, developing PTF was hardly a one-man job. It takes support, criticism, and testing to produce reusable code. I'd like to thank David Potochnak, Chanakya Ganguly, Dan McCartney, and Anne Jata for helping make this framework possible.

References

Becker, Pete. "Writing Multi-threaded Applications in C++." C++ Report (February 1994).

Lockhart, Harold W. OSF DCE. New York, NY: McGraw Hill, 1994.

Open Software Foundation. OSF DCE Application Development Reference. Englewood Cliffs, NJ: Prentice-Hall, 1993.

Open Software Foundation. OSF DCE Application Development Guide. Englewood Cliffs, NJ: Prentice-Hall, 1993.

Using Threads Effectively

Because a typical program can have more than 100 threads, using threads properly requires synchronization. Two mechanisms are available: mutual exclusion locks (mutexes) and condition variables. A mutex is used to protect data from simultaneous access by multiple threads (race condition). A thread would lock a mutex associated with a piece of data, update the data, then unlock the mutex. You'll need to guard against deadlocks. Deadlocks usually occur when working with more than one mutex. Imagine a problem where both threads A and B need to lock both mutexes 1 and 2. Thread A grabs mutex 1 and thread B grabs mutex 2. Both threads will wait indefinitely for the other's mutex to become available. You can also deadlock on one mutex if you attempt to lock the same mutex a second time, say in a recursive routine. DCE, however, supports a special recursive mutex for just such a situation.

A condition variable can be considered as a kind of mutex, except that instead of synchronizing threads with data, it synchronizes threads with other threads. For example, if thread A needs an intermediate value calculated by thread B, thread A can wait for thread B to complete its computations. Thread B, when ready, can then signal thread A. This signal is not to be confused with UNIX signals.

By default, DCE threads are scheduled with a round-robin (RR) policy in medium priority. If all your threads run at the same priority, the RR policy ensures that all threads get serviced eventually. Don't assume, however, that time-slicing occurs at a fine level (millisecond or smaller). If your platform has multiple CPUs, threads can run simultaneously, but on a single-CPU platform, time slicing can occur on the order of a minute or more, depending on your system configuration. This seemingly large time slice is efficient; context switching between threads can be expensive.

DCE threads also support a first-in, first-out (FIFO) policy, whereby a thread will run uninterrupted by any other thread of equal priority. The thread will yield only when it's completed or when it blocks for I/O. FIFO is the most efficient policy, because it minimizes context switching. It is also the least fair, because short-running threads will have to wait for long-running threads. Long-running threads can, however, give up control with intelligently placed calls to pthread_yield(). Realize, though, that if the long running thread has locked a mutex needed by other threads, pthread_yield() will accomplish nothing and cost a context switch. Other scheduling policies are available, but RR and FIFO are the most commonly used ones.

While the nature of an application will determine the proper scheduling policy, when it comes to thread priorities, simple is better. Use threads of identical priority or one high-priority thread and many with medium priority. Writing a program with clever scheduling priorities generally leads to unexpected performance issues. One such case is priority inversions, where a lower-priority thread can run before a higher-priority thread. For example, given three threads of different priorities, if low-priority thread C locks a mutex needed by high-priority thread A and thread C waits while medium-priority thread B runs, thread A must wait while thread B runs.

Often, a low-priority thread can be replaced with a medium-priority thread that sleeps a lot. For example, a low-priority thread might make sense for such tasks as monitoring the state of a file, but a better solution would be to create the thread with the same priority as all the other threads (medium) and have it periodically "sleep," or wait on a condition variable, for n seconds before checking the file. In addition, if the state of the file were to be changed by another thread, the thread making the change could wake (signal) the sleeping thread. Putting a thread to sleep avoids polling in a tight loop looking for work to do, thus saving CPU cycles and simulating a lower-priority schedule.

Working with threads isn't easy. Their simultaneous execution and interaction often lead to conditions that are difficult to foresee. Troubleshooting a threaded application can be arduous if you don't have a debugger that can trace through threads. As of this writing, HP has a debugger for threads in beta. For the most part, I relied on old-fashioned debugging statements that also displayed the thread ID. I also developed and depended on PTF, a framework for pthreads, to reduce my chances for error. It's evident that threads require a fair amount of work, but the results are well worth the effort.

--M.Y.

Table 1: PTF classes and the PTTSafeType template.


<b>Class     Purpose</b>
<I>PTObject</I>     Threads are derived from this class.
<I>PTMutex</I>     Encapsulates DCE mutexes.
<I>PTCondVar</I>     Derived from <I>PTMutex</I>. Encapsulates DCE condition variables.
<I>PTSafeObject</I>     A thread-safe class can be derived from this class.
<I>PTLock</I>     Works with <I>PTSafeObject</I> to lock and unlock a mutex.
PTTSafeType     Template derived from <I>PTSafeObject;</I> makes built-in types thread safe.

Figure 1: Code fragment using PTLock to make your derived class thread safe.

derivedClass::updateData()
{
    // Class is derived from PTSafeObject
    PTLock lock(this);  // mutex locked
    // safe to update data here.
    ...
    // return invokes PTLock's destructor
    // which unlocks the mutex.
}
Figure 2: Code fragment illustrating thread-safe template.
PTTSafeType <int> sequenceNumber;
 ...
sequenceNumber.set (0);      // sets number to 0 safely.
++sequenceNumber;           // inc number safely.
// or sequenceNumber++;
 ...
 

Listing One

/***** PTF.H -- Header file describes classes for pthread framework. *****/
#ifndef PTF_H
#define PTF_H
extern "C"
{
#include <pthread.h>
}
#define TRUE    1
#define FALSE   0
/*--- CLASS: PTMutex. Description: Create, destroy, lock, unlock a mutex. ---*/
class PTMutex 
{
public:
    PTMutex ();
    virtual ~PTMutex();
    int lock();
    int unlock();
    int tryLock();
    // dummy declarations to prevent copying by value. classes not having 
    // pointer members may remove these declarations. Note: operator=() 
    // should be chained in case of inheritance.
    PTMutex (const PTMutex &);
    PTMutex& operator=(PTMutex&);
    virtual const char *className() { return "PTMutex"; }
    virtual int isValid();  
protected:
    pthread_mutex_t _hMutex;        // handle to mutex
private:
    int _validFlag;
};
/*--- CLASS: PTCondVar. 
 *--- Description: Manages condition variables and associated mutexes. ----*/
class PTCondVar : public PTMutex
{
public:
    PTCondVar ();               // pthread_cond_init()
    virtual ~PTCondVar ();      // pthread_cond_destroy()
    int signal ();
    int broadcast ();
    int timedWait (int seconds=0);
    pthread_cond_t hCondVar() { return _hCondVar; }
    // dummy declarations to prevent copying by value.
    // classes not having pointer members may remove these declarations.
    // Note: operator=() should be chained in case of inheritance.
    PTCondVar (const PTCondVar&);
    PTCondVar& operator=(PTCondVar&);
    const char *className() { return "PTCondVar"; }
    virtual int isValid();  
protected:
    pthread_cond_t _hCondVar;       // handle to condition variable
private:
    int _validFlag; 
};
/*-- CLASS: PTObject. Description: Abstract class. Use to build a pthread. --*/
class PTObject 
{
public:
    PTObject ();            // use default schedule & policy
    virtual ~PTObject ();
    int start ();
    virtual int runPThread() = 0;   // this gets called by start_routine()
    int tid () {return _tid;}
    int join ();
    // dummy declarations to prevent copying by value.
    // classes not having pointer members may remove these declarations.
    // Note: operator=() should be chained in case of inheritance.
    PTObject (const PTObject &);
    PTObject& operator=(const PTObject&);
    
    const char *className() { return "PTObject"; }
    virtual int isValid();  
    pthread_addr_t exitStatus;      // thread's exit code (used by join()).
protected:
    pthread_t _hPThread;            // handle to thread
    // this static routine gets passed to pthread_create()
    static pthread_addr_t start_routine(void *obj);
private:
    int _validFlag;
    int _tid;
};
/*--- CLASS: PTSafeObject. Description: Derive from this to create a 
 *--- thread-safe class. ----*/
class PTSafeObject 
{
public:
    PTSafeObject ();
    ~PTSafeObject ();
    PTMutex *pPTMutex() const;
    // dummy declarations to prevent copying by value.
    // classes not having pointer members may remove these declarations.
    // Note: operator=() should be chained in case of inheritance.
    PTSafeObject (const PTSafeObject &);
    PTSafeObject& operator=(const PTSafeObject&);
    
    const char *className() { return "PTSafeObject"; }
    virtual int isValid();  
protected:
    class PTLock
    {
    public:
        PTLock (PTSafeObject *ThreadSafe);
        ~PTLock ();
    private:
        PTMutex *_pPTLockMutex;
    };
private:
    // friend declaration needs to be here for nested classes. 
    // might be an HP compiler bug.
    friend class PTLock;
    PTMutex *_pPTMutex;
    int _validFlag;
};
#endif

Listing Two

/***** PTF.CPP -- Encapsulation of DCE PThreads. Classes include: PTObject,    
   derive from this to create your threads; PTMutex, creates a mutex;
   PTCondVar, derived from PTMutex. Creates a condition variable and 
   and an associated mutex; PTSafeObject, derive from this for classes 
   which update shared resources; PTLock, locks a mutex. Works with 
   PTSafeObject. Currently supports default thread creation: round-robin 
   scheduling and medium priority. Currently supports default mutex 
   creation: fast locks (as opposed to recursive and non-recursive locks). 
***************************************************************/
#include "PTF.H"
#include <assert.h>
#include <sys/errno.h>
#ifndef NDEBUG
#include <stdio.h>
#endif
extern int errno;
/*--- Function Name: PTObject::PTObject. Description: Constructor using 
 *--- default thread scheduling (Round-robin) and priority (medium). 
 *--- Returns: None ----*/
PTObject::PTObject()
{
    _validFlag = FALSE;
    _tid = 0;                   // id assigned when thread is created
    exitStatus = 0;             // initial thread return code
    _validFlag = TRUE;
}
/*--- Function Name: PTObject::~PTObject. Description: Destructor. Free 
 *--- resources allocated to PThread. Returns: None ---*/
PTObject::~PTObject()
{
    pthread_cancel (_hPThread);     // issue a cancel message to thread
    pthread_detach (&_hPThread);    // free resources of cancelled thread
}
/*---- Function Name: PTObject::isValid. Description: Return private variable
 *---- _validFlag. The variable indicates the state of the object, whether it 
 *---- is valid or not. Returns: TRUE or FALSE ---*/
int
PTObject::isValid()
{
    return _validFlag;
}
/*--- Function Name: PTObject::join. Description: join() causes the calling 
 *--- thread to wait for the thread object to complete. See pthread_join() in 
 *--- DCE Dev. Ref. When the thread is complete, the thread's return code is 
 *--- stored in a public variable: exitStatus. Returns: 0, success; -1, Error. 
 *--- Check errno. ---*/ 
int
PTObject::join ()
{   
    pthread_t threadID = pthread_self();
    int uniqueID = pthread_getunique_np (&threadID);
    if (uniqueID == tid())
    {
        printf ("TID %d: Can't join thread to itself.\n", uniqueID);
        return -1;
    }
    return pthread_join (_hPThread, &exitStatus);
}
/*--- Function Name: PTObject::start. Description: Explicitly starts the 
 *--- thread. Actually, thread creation is performed here as well. If thread 
 *--- were created in the constructor, thread may start before a derived class
 *---  had a chance to complete its constuctor which would lead to 
 *--- initialization problems. Returns: 0, success; -1, fail (errno = 
 *--- EAGAIN || ENOMEM) ---*/ 
int 
PTObject::start()
{
    // Create a thread using default schedule & priority. Also, pass in *this 
    // ptr for argument to associate thread with an instance of this object.
    int status = pthread_create (&_hPThread, pthread_attr_default,
            (pthread_startroutine_t)&PTObject::start_routine,
            (void *)this);
    if (status == 0)
        _tid = pthread_getunique_np (&_hPThread);
   return status;
}
/*--- Function Name: PTObject::start_routine. Description: Static function is 
 *--- passed into pthread_create. It is the start of the thread routine. In 
 *--- turn, it calls the virtual function runThread() which is written by the 
 *--- user of this class. Returns: None ---*/ 
pthread_addr_t 
PTObject::start_routine (void *obj)
{
    // get object instance
    PTObject *threadObj = (PTObject *)obj;
    int status = threadObj->runPThread();
    return (pthread_addr_t)status;
}
/*--- Function Name: PTMutex::PTMutex. Description: Constructor creates a 
 *--- mutex with a default attribute (fast mutex). Returns: None ---*/
PTMutex::PTMutex()
{
    _validFlag = FALSE;
    int status = pthread_mutex_init (&_hMutex, pthread_mutexattr_default);
    if (status == -1)
       return;
    _validFlag = TRUE;
}
/*--- Function Name: PTMutex::~PTMutex. Description: Destructor destroys mutex.
 *--- Assumes mutex is unlocked. DCE doesn't provide a direct way to determine
 *--- the state of a mutex. In case of failure, use assert() macro. 
 *--- Returns: None ---*/ 
PTMutex::~PTMutex()
{
    // assumes mutex is unlocked. DCE doesn't provide a direct way to determine
    // the state of a lock so I'll just try to destroy it without any checks.
    // I'm using a long name for the return value so that
    // "assert" macro is self documenting.
    int ipthread_mutex_destroy = pthread_mutex_destroy (&_hMutex);
#ifndef NDEBUG
    pthread_t threadID = pthread_self();
    int tid = pthread_getunique_np (&threadID);
    if (ipthread_mutex_destroy == -1)
        printf ("TID %d: Could not destroy mutex. errno=%d\n", tid, errno);
    assert (ipthread_mutex_destroy == 0);
#endif  
}
/*--- Function Name: PTMutex::isValid. Description: Used to determine if 
 *--- object has been constructed successfully. Returns: TRUE or FALSE ---*/ 
int
PTMutex::isValid()
{
    return _validFlag;
}
/*--- Function Name: PTMutex::lock. Description: Lock this mutex. If mutex is 
 *--- already locked, wait for it to become available. Returns: 0, success;
 *--- -1, fail  (errno = EINVAL or EDEADLK) ---*/ 
int
PTMutex::lock()
{
    return pthread_mutex_lock (&_hMutex);      
}
/*--- Function Name: PTMutex::trylock. Description: Try and lock this mutex. 
 *--- If mutex is already locked, do not wait for it to become available. 
 *--- Just return. Returns: 1, success; 0, mutex already locked; -1,  fail, 
 *--- mutex handle invalid ---*/ 
int
PTMutex::tryLock()
{
    return pthread_mutex_trylock (&_hMutex);
}
/*--- Function Name: PTMutex::unlock. Description: Unlock this mutex.
 *--- Returns: 0, success; -1,  fail, invalid mutex handle ---*/ 
int
PTMutex::unlock()
{
    return pthread_mutex_unlock (&_hMutex);    
}
/*--- Function Name: PTCondVar::PTCondVar. Description: Constructor creates a 
 *--- condition variable. Returns: None ---*/ 
PTCondVar::PTCondVar()
{
    _validFlag = FALSE;
    int status = pthread_cond_init (&_hCondVar, pthread_condattr_default);
    if (status == -1)
        return;         // errno = EAGAIN or ENOMEM
    _validFlag = TRUE;
    return;
}
/*--- Function Name: PTCondVar::~PTCondVar. Description: Destructor destroy a 
 *--- condition variable.  It can fail if the condition variable is busy. 
 *--- Returns: None --*/
PTCondVar::~PTCondVar()
{
    int ipthread_cond_destroy = pthread_cond_destroy (&_hCondVar);
#ifndef NDEBUG
    pthread_t threadID = pthread_self();
    int tid = pthread_getunique_np (&threadID);
    if (ipthread_cond_destroy == -1)
        printf ("TID %d: Could not destroy condition variable. errno=%d\n", 
                                                                   tid, errno);
    assert (ipthread_cond_destroy == 0);
#endif  
}
/*--- Function Name: PTCondVar::broadcast. Description: Wakes all threads 
 *--- waiting on a condition variable object. Calling this routine means that 
 *--- data is ready for a thread to work on. A broadcast lets one or more 
 *--- threads proceed. Returns: 0, success; -1, fail (errno = EINVAL) ---*/ 
int
PTCondVar::broadcast()
{
    return pthread_cond_broadcast (&_hCondVar);
}
/*--- Function Name: PTCondVar::isValid. Description: Used to determine if 
 *--- constructor succeeded or not. Returns: TRUE or FALSE ---*/ 
int
PTCondVar::isValid()
{
    return _validFlag;
}
/*--- Function Name: PTCondVar::signal. Description: Wakes one thread waiting 
 *--- on a condition variable. Thread to wake is determined by its scheduling 
 *--- policy. Returns: 0, on success; -1, on failure (errno = EINVAL) ---*/ 
int
PTCondVar::signal()
{
    return pthread_cond_signal (&_hCondVar);
}
/*--- Function Name: PTCondVar::timedWait. Description: Will wait on a 
 *--- mutex/condition variable until thread receives a signal or broadcast, or
 *--- until specified number of seconds have elapsed, whichever comes first.
 *--- 0 seconds means wait forever. (default) Returns: 0, success. Thread was 
 *--- signalled; 1, wait timed out. No signal; -1, wait failed. See errno ---*/
int
PTCondVar::timedWait (int seconds)
{
    int status;
    lock();         // lock this condition vars' mutex
    if (seconds <= 0)
    {
        // thread will wait here until it gets a signal
        // 0, default value, means wait forever.
        status = pthread_cond_wait (&_hCondVar, &_hMutex);
    }
    else
    {
        // wait for specified number of seconds
        // use non-portable dce routine to get absolute time from seconds.
        struct timespec delta;
        struct timespec abstime;
        
        delta.tv_sec = seconds;
        delta.tv_nsec = 0;
        
        // I'm using a long name for the return value so if "assert"
        // aborts, message is self-documenting.
        int  ipthread_get_expiration_np = pthread_get_expiration_np (&delta, &abstime);
        assert (ipthread_get_expiration_np == 0);   
        // thread will wait here until it gets a signal or
        // until abstime value is reached by system clock.
        status = pthread_cond_timedwait (&_hCondVar, &_hMutex, &abstime);
        if (status == -1 && errno == EAGAIN)
           status = 1;      // lock timed-out
    }
    unlock();       // unlock internal mutex
    return status;
}
/*--- Function Name: PTSafeObject::PTSafeObject. Description: Used to make a 
 *--- class thread-safe. Derive from this class, then add PTLock (this); to 
 *--- the first line of each member function in your derived class that 
 *--- accesses data. This class forms the outer part of a thread-safe class. 
 *--- It creates and deletes a PTMutex object. The inner class (nested class 
 *--- -- PTLock) locks and unlocks a PTMutex object. Returns: None ---*/
PTSafeObject::PTSafeObject()
{
    _validFlag = FALSE;
    _pPTMutex = new PTMutex;
    if (!_pPTMutex->isValid())
       return;
    _validFlag = TRUE;
    return;
}
/*--- Function Name: PTSafeObject::~PTSafeObject. Description: Delete internal
 *--- PTMutex object. Returns: None ---*/ 

PTSafeObject::~PTSafeObject()
{
    delete _pPTMutex;
}
/*--- Function Name: PTSafeObject::pPTMutex. Description: Retrieve a copy of 
 *--- the internal PTMutex object. Returns: None ---*/ 
PTMutex *
PTSafeObject::pPTMutex() const
{
    return _pPTMutex;
}
/*--- Function Name: PTSafeObject::isValid. Description: Determine if 
 *--- constructor succeeded or not. Returns: TRUE or FALSE. ---*/ 
int
PTSafeObject::isValid()
{
    return _validFlag;
}
/*--- Function Name: PTSafeObject::PTLock::~PTSafeObject::PTLock. Description:
 *--- Destructor for class nested within PTSafeObject. It just unlocks the 
 *--- PTMutex object. The outer class, PTSafeObject, deletes it. 
 *---  Returns: None ---*/ 
PTSafeObject::PTLock::~PTLock()
{
    (void)_pPTLockMutex->unlock();
}
/*--- Function Name: PTSafeObject::PTLock::PTLock. Description: This class 
 *--- forms inner (nested) class of a thread-safe object. The object is 
 *--- responsible for locking and unlocking a PTMutex object. This constructor
 *--- locks it. A user should not instantiate this object explicitly. Pass
 *--- a "this" pointer to this function to give access to outer class' private
 *--- variables. The outer part (PTSafeObject) creates and deletes a PTMutex 
 *--- object. Returns: None ---*/ 
PTSafeObject::PTLock::PTLock(PTSafeObject *ThreadSafe)
{
    _pPTLockMutex = ThreadSafe->_pPTMutex;
    (void)_pPTLockMutex->lock();
}

Listing Three

/***** PTTF.H -- Template to create thread-safe types. *****/
/*--- TEMPLATE: PTSafeType. Description: This inherits from class PTSafeObject.
 *--- Implemented as template, it can make a variety of types threadsafe.---*/ 
#ifndef PTTF_H
#define PTTF_H
#include <PTF.H>
template <class T>
class PTTSafeType : public PTSafeObject
{
public:
    void operator=(T threadSafeType) {set(threadSafeType);}
    operator T () {return get();}
    T operator ++();
    T operator --();
    T operator ++(T threadSafeType);
    T operator --(T threadSafeType);
    T get();
    T set(T threadSafeType);
private:
    T _threadSafeType;
};
#ifdef RW_COMPILE_INSTANTIATE
#include "PTTF.CC"
#endif
#endif

Listing Four

/**** PTTF.CC -- Template definition for PTTSafeType ****/
#ifndef PTTF_CC
#define PTTF_CC
/*--- Function Name: get. Description: retrieves a value safely in a threaded 
 *--- environment. Note that the value is invalid if a set() has never been 
 *--- called. Returns: value T. ---*/ 
template <class T> T PTTSafeType<T>::get()
{
    PTLock Lock(this);
    return _threadSafeType;
}
/*--- Function Name: set. Description:  sets a value safely in a threaded 
 *--- environment. Returns: previous value of T. ---*/ 
template <class T> T PTTSafeType<T>::set (T threadSafeType)
{
    PTLock Lock(this);
    T previous;
    previous = _threadSafeType;
    _threadSafeType = threadSafeType;
    return previous;
}
/*--- Function Name: prefix and postfix operators: ++ and --. Description: 
 *--- Increment and decrement a value safely in a threaded environment
 *--- Returns: value of T after incrementing or decrementing. ---*/ 
template <class T> T PTTSafeType<T>::operator ++()
{
    PTLock Lock(this);
    return ++_threadSafeType;
}
template <class T> T PTTSafeType<T>::operator --()
{
    PTLock Lock(this);
    return --_threadSafeType;
}
template <class T> T PTTSafeType<T>::operator ++(T threadSafeType)
{
    PTLock Lock(this);
    return _threadSafeType++;
}
template <class T> T PTTSafeType<T>::operator --(T threadSafeType)
{
    PTLock Lock(this);
    return _threadSafeType--;
}
#endif
End Listings>>


Copyright © 1995, Dr. Dobb's Journal


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.