11.1 Taking Advantage of Interface Classes

It is often advantageous to use encapsulation to hide the details of function libraries and to provide self-contained components that can be reused. Let's take for example the mutex that we discussed in Chapter 7. Recall that a mutex is a special kind of variable used for synchronization. Mutexes are used to provide safe access to a critical section of data or code within a program. There are six basic functions that can be performed on a pthread_mutex_t (POSIX Threads Mutex) variable.

Synopsis

[View full width]
 
[View full width]
#include <pthread.h> pthread_mutex_destroy(pthread_mutex_t *mutex); pthread_mutex_init(pthread_mutex_t *mutex, pthread_mutexattr_t graphics/ccc.gif *attr); pthread_mutex_lock(pthread_mutex_t *mutex); pthread_mutex_timedlock(pthread_mutex_t *mutex); pthread_mutex_trylock(pthread_mutex_t *mutex); pthread_mutex_unlock(pthread_mutex_t *mutex);

Each of these functions take at least a pointer to a pthread_mutex_t variable. An interface class can be used to encapsulate access to the pthread_mutex_t variable and to simplify the function calls that access the pthread_mutex_t variable. In Example 11.1, we can declare a class called mutex .

Example 11.1 Declaration of the mutex class.
 class mutex{ protected:    pthread_mutex_t *Mutex;    pthread_mutexattr_t *Attr; public:    mutex(void)    int lock(void);    int unlock(void);    int trylock(void);    int timedlock(void); }; 

Once this mutex class is declared, we can use it to define mutex variables. We can declare arrays of these mutexes. We use these variables as members of user -defined classes. By encapsulating the pthread_mutex_t variable and its functions, we can take advantage of the object-oriented programming techniques. These mutex variables can now be used as parameter arguments and function return values. And since the functions are now bound to the pthread_mutex_t variable, wherever we pass the mutex variable the functions are also available.

The member functions for the class mutex are defined by wrapping the calls to the corresponding Pthread routines, for instance:

Example 11.2 Member functions for the mutex class.
 mutex::mutex(void) {    try{          int Value;          Value = pthread_mutexattr_int(Attr);          //...          Value = pthread_mutex_init(Mutex,Attr);          //...    } } int mutex::lock(void) {    int RetValue;    RetValue = pthread_mutex_lock(Mutex);    //...    return(ReturnValue); } 

We also protect the pthread_mutex_t * and the pthread_mutexattr_t * through encapsulation. In other words, when we invoke the lock() , unlock() , trylock() , and so on methods , we don't have to worry about which mutex variable or which attribute variables these functions will be applied to. The availability of information hiding through encapsulation allows the programmer to write safer code. With the free floating versions of Pthread mutex functions, any pthread_mutex_t variable may be passed to these functions. Simply passing the wrong mutex to one of these functions can lead to deadlock or indefinite postponement. Encapsulating the pthread_mutex_t variable and the pthread_mutexattr_t variable within the mutex class gives the programmer complete control over which functions have access to those particular variables.

Now we can use an embedded interface class like mutex within other userdefined classes to provide thread-safe classes. Let's say that we wanted to make a thread-safe queue and a thread-safe pvm_stream class. The queue is used to store incoming events for multiple threads within a program. Some of the threads have the responsibility of sending messages to various PVM tasks. The PVM tasks and the threads are executing concurrently. The multiple threads share a single PVM stream and a single event queue. Figure 11-1 shows the relationship between threads, PVM tasks , event queue, and pvm_stream .

Figure 11-1. The relationship between threads, PVM tasks, event queue, and the pvm_stream class within a PVM program.

graphics/11fig01.gif

The queue in Figure 11-1 is a critical section because it is shared between multiple executing threads. The pvm_stream in Figure 11-1 is also a critical section because it is shared between multiple executing threads. If these critical sections are not synchronized and protected, then we can end up with corrupted data in the queue and pvm_stream . The fact that multiple threads can update either the queue or the pvm_stream introduces an environment for race conditions. To help manage the race conditions we design our queue and pvm_stream class with built-in lock and unlock functionality. The lock and unlock functionality is supplied by our mutex class. Figure 11-2 shows the class diagram for our user-defined x_queue and pvm_stream classes.

Figure 11-2. The class diagram for our userdefined x_queue class and pvm_stream class.

graphics/11fig02.gif

Notice that the x_queue class contains a mutex class. This is a has-a or aggregation relationship between x_queue and mutex , that is, x_queue has a mutex class. Any operation that changes the state of our x_queue class can cause a race condition if that operation is not synchronized. Therefore, the operations that add an object to the queue or that remove an object from the queue are candidates for synchronization. Example 11.3 contains the declaration of our x_queue class as a template class.

Example 11.3 Declaration of the x_queue class.
 template <class T> x_queue class{ protected:    queue<T> EventQ;    mutex Mutex;    //... public:    bool enqueue(T Object);    T dequeue(void);    //... }; 

The enqueue() method is used to add items to the queue and the dequeue() method is used to remove items from the queue. Each of these methods will use the Mutex object. The enqueue() and dequeue() methods are defined in Example 11.4.

Example 11.4 Defintion of enqueue() method.
 template<class T> bool x_queue<T>::enqueue(T Object) {    Mutex.lock();    EventQ.push(Object);    Mutex.unlock(); } template<class T> T x_queue<T>::dequeue(void) {    T Object;    //...    Mutex.lock();    Object = EventQ.front()    EventQ.pop();    Mutex.unlock();    //...    return(Object); } 

Now items can be added to and removed from our queue in a multithreaded environment. Thread B in Figure 11-1 adds items to the queue and Thread A in Figure 11-1 removes items. The mutex class is an interface class. It wraps the pthread_mutex_lock() , pthread_mutex_unlock() , pthread_mutex_init() , and pthread_mutex_trylock() functions. The x_queue class is also an interface class because it adapts the interface for the built-in queue<T> class. First, it changes the push() and pop() method interfaces to enqueue() and dequeue() . Furthermore, it wraps the insertion and removal of items with the Mutex.lock() and Mutex.unlock() methods. So in the first case we use the interface class to encapsulate pthread_mutex_t* and pthread_mutexattr_t* variables. We also wrap several functions from the Pthread library. In the second case, we use the interface class to adapt the interface of the queue<T> class. Another advantage of the mutex class is that it can be easily reused in other classes that contain critical sections or critical regions .

In Figure 11-1 the PVM stream is also a critical section because both Thread A and Thread B have access to the stream. A possible race condition exists because Thread A and Thread B may both try to access the stream at the same time. Therefore, we use the mutex class in our user-defined pvm_stream class to provide synchronization.

Example 11.5 Declaration of pvm_stream class.
 class pvm_stream{ protected:    mutex Mutex;    int TaskId;    int MessageId;    //... public:    pvm_stream & operator <<(string X);    pvm_stream & operator <<(int X);    pvm_stream &operator <<(float X);    pvm_stream &operator>>(string X);    //... }; 

As with the x_queue class, the Mutex object is used with the functions that can change the state of a pvm_stream object. For example, we might define one of the << operators as:

Example 11.6 Definition of the << operator for the pvm_stream class.
 pvm_stream &pvm_stream::operator<<(string X) {    //...    pvm_pkbyte(const_cast<char *>(X.data()),X.size(),1);    Mutex.lock();    pvm_send(TaskId,MessageId);    Mutex.unlock();    //...    return(*this); } 

The pvm_stream class uses Mutex objects to synchronize access to its critical section in the same manner as was done with the x_queue class. It's important to note that in both cases the pthread_mutex functions are hidden. The programmer does not have to be concerned about their syntax. A simpler lock() and unlock() interface is used. Furthermore, there is no confusion about which pthread_mutex_t * is being used with the pthread_mutex functions. In addition to these advantages, the programmer may declare multiple instances of the mutex class without having to call Pthread mutex functions over again. We made reference to the Pthread functions once within the method definitions of the mutex class. Now only the methods of the mutex class need be called.



Parallel and Distributed Programming Using C++
Parallel and Distributed Programming Using C++
ISBN: 0131013769
EAN: 2147483647
Year: 2002
Pages: 133

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net