11.2 A Closer Look at Object-Oriented Mutual Exclusion and Interface Classes

To confront some of the complexity with writing and maintaining programs that require concurrency, we try to streamline and simplify the API to the parallel libraries. Some systems may require the use of the Pthreads library, the MPI library, the standard semaphore, and shared memory functions as part of a single solution. Each of these libraries and functions have their own protocols and syntax. However, often they have similar functionality. We can use interface classes, inheritance, and polymorphism to present a simplified and consistent interface to the programmer. We can also hide the details of library-specific implementation from our applications. If the application only relies on the methods used in our interface classes, then our application is shielded from implementation changes, library updates, and other under-the-hood restructuring. Ultimately the work that you do in providing interface classes to concurrency components and function libraries and function data will allow you to reduce the complexity of parallel programming. Let's take a closer look at how we can approach the design of interface classes that support concurrency.

11.2.1 Semi-Fat Interfaces that Support Concurrency

The basic POSIX semaphore is used to synchronize access to a critical section between two or more processes. The basic POSIX thread is used to synchronize access to a critical section between two or more threads. In both cases, there are synchronization variables involved and a number of functions available on the synchronization variables . The MPI library and the PVM library both contain message-passing primitives. Both have the capabilities of spawning tasks . However, the interfaces of these two libraries are different. The application programmer wants to focus on the logic and structure of the program. This is difficult when the semantics of a program are obscured by multiple libraries that happen to perform similar functions but whose syntax and protocols are very different. What is needed is a generalized interface that can be used across libraries.

There are at least a couple of approaches to designing an interface for a family of classes or a collection of classes. The object-oriented approach starts with the general and moves to the specific by means of inheritance. That is, we take the minimal core set of characteristics and attributes that every member of the family of classes should have and, through lineage of inheritance, we specialize those characteristics for each class. In this approach, the interface grows more narrow as you move down the class hierarchy. The second approach is often used in template collections. Template-based approaches start with the specific and move to the general through fat interfaces. The fat interface includes a generalization of all of the characteristics and attributes under discussion (see Stroustrup, 1997). If we were to apply narrow and fat interfaces to our concurrency libraries, the narrow interface approach would take intersection between each library, generalize it, and put it in a base class. The fat interface approach would take the union of the functionality within each library, generalize it, and put in a base class. The set intersection would produce a smaller, less useful class. The set union would produce a large, possibly unwieldy class. For our discussion we are interested in a position somewhere in the middle. We want semi-fat interfaces. We start with a narrow approach and generalize as much as we can within a single class hierarchy. We then use the narrow interface as a basis for a collection of classes that are not related by inheritance but that are related by function. The narrow interface acts as a sort of policy to constrain how fat a semi-fat interface can become. In other words, we don't want a union of every characteristic and attribute under discussion; we only want a union of the things that are logically related to our narrow interface. Let's illustrate this point with a simple design of interface classes for the Pthread mutex, Pthread read-write lock variable, and the POSIX semaphore.

Regardless of the implementation details, the operations of lock, unlock, and trylock are characteristic of synchronization variables. So we make a base class that will act as a pattern for a family of classes. The synchronization_variable class is declared in Example 11.7.

Example 11.7 Declaration of the synchronization_variable class.
 class synchronization_variable{ protected:    runtime_error Exception;    //... public:    int virtual lock(void) = 0;    int virtual unlock(void) = 0;    int virtual trylock(void) = 0;    //... }; 

Notice that the methods of the synchronization_variable class are declared virtual and are initialized to . This means these methods are pure virtual methods, making the synchronization_variable class an abstract class. An object cannot be directly created from any class that has one or more pure virtual functions. In order to use this class a new class must be derived from it and the new class must provide definitions for each of the pure virtual functions. The abstract class acts as a kind of policy that says what functions a derived class must define. It provides an interface blueprint for derived classes. It doesn't dictate how the methods should be implemented, only that the methods must be present and cannot be pure virtuals. We can get hints of the proposed behavior from the names of the methods. The blueprint interface class provides an interface without any implementation. This type of class is used to provide a foundation for future classes. The blueprint class guarantees that the interface will have a certain look (Caroll & Ellis, 1995). The synchronization_variable class provides a blueprint interface policy for our family of synchronization variables. We use inheritance to provide implementations for the interface. The Pthread mutex is a good candidate for an interface class, so we define a mutex class derived from synchronization_variable :

Example 11.8 Declaration of a mutex class that inherits the synchronization_variable class.
 class mutex : public synchronization_variable{ protected:    pthread_mutex_t *Mutex;    pthread_mutexattr_t *MutexAttr;    //... public:    int lock(void);    int unlock(void);    int trylock(void);    //... }; 

The mutex class will provide implementations for each of the pure virtual functions. Once the functions are defined, the policy suggested by the abstract base class has been met. The mutex class is not considered an abstract class, therefore mutex and any of its descendants can be instantiated as objects. Each of the methods of mutex wraps the corresponding Pthread function. For instance:

 int mutex::trylock(void) {    //...    return(pthread_mutex_trylock(Mutex);    //... } 

provides an interface to the pthread_mutex_trylock() function. The lock() , unlock() , and trylock() interface simplifies the Pthread function calls. Our goal is to use encapsulation and inheritance to eventually define a complete family of mutex classes. The inheritance process is a specialization process. The derived class provides additional attributes or characteristics that distinguish it from its ancestors . Each attribute or characteristic added to the derived class specializes it. Now we can design a specialization of the mutex class through inheritance by adding the notion of a read/write mutex class. Our generic mutex class is designed to protect a critical section from access. Once a thread has locked the mutex, it has access to the critical section the mutex protects. Sometimes this is too extreme. There are times when it is okay to allow multiple threads to access the same data at the same time, so long as none of the threads modify or change the data in any way. That is, there are times that we may want to relax the lock on the critical section and only lock out access to actions that want to modify or change the data and allow access to actions that only read or copy the data. This is called a read lock . The read lock allows concurrent read access to a critical section. The critical section may already be locked by one thread and another thread may also obtain a lock so long as it does not want to modify the data. The critical section may be locked for writing by some thread, and another thread may request a lock for reading the critical section.

The blackboard architecture is a good example of a structure that can take advantage of read mutexes and the stronger, more generic mutex. The blackboard is a common region shared by concurrently executing routines. The blackboard is used to hold solutions to some problem that the group of routines is collaboratively solving. As each routine makes progress toward the solution to the problem, it writes its progress to the blackboard. Each routine also reads the blackboard to see if there are any results generated by the other routines that might be useful. The blackboard structure is a critical section. We really only want one routine at a time to update the blackboard. On the other hand, we can allow any number of routines to simultaneously read the blackboard. Also, if we have multiple routines reading the blackboard, we don't want the blackboard updated until all the routines that are reading are done. The read mutex is an appropriate mutex for this situation because it can lock access to the blackboard and only allow blackboard readers while denying access to blackboard writers. However, the blackboard will need to be updated if a solution to the problem is ever to be achieved. When the blackboard is being updated, we do not want any readers to have access to the blackboard. We want to block the readers until the routine that is updating the blackboard is done. Therefore, we need a write mutex. Only one routine may hold a write mutex at a time. So we distinguish between a mutex that is locked for reading and no writing and a mutex that is locked for writing and no reading. With a read mutex we can have multiple concurrent reads, and with a write mutex we may only have one writer. This is part of the CREW (Concurrent Read Exclusive Write) approach to parallel programming.

Synopsis

 #include <ptrhead.h> int pthread_rwlock_init(pthread_rwlock_t *,                         const pthread_rwlockattr_t *); int pthread_rwlock_destroy(pthread_rwlock_t *); int pthread_rwlock_rdlock(pthread_rwlock_t *); int pthread_rwlock_tryrdlock(pthread_rwlock_t *); int pthread_rwlock_wrlock(pthread_rwlock_t *); int pthread_rwlock_trywrlock(pthread_rwlock_t *); int pthread_rwlock_unlock(pthread_rwlock_t *); int pthread_rwlockattr_init(pthread_rwlockattr_t *); int pthread_rwlockattr_destroy(pthread_rwlockattr_t *); int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t *,                                   int *); int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *, int); 

To design our specialization of the mutex class, we need to add the ability to perform read locks and write locks. The pthreads library has a read/write mutex variable and attribute variable:

 pthread_rwlock_t and pthread_rwlockattr_t 

These variables are used in conjunction with the 11 pthread_rwlock() functions. We use our interface class rw_mutex to encapsulate the pthread_rwlock_t and pthread_rwlockattr_t variables and to wrap the Pthread read/write mutex functions.

Example 11.9 Declaration of rw_mutex class that contains pthread_rwlock_t and pthread_rwlockattr_t objects.
 class rw_mutex : public mutex{ protected:    struct pthread_rwlock_t *RwLock;    struct pthread_rwlockattr_t *RwLockAttr; public:    //...    int read_lock(void);    int write_lock(void);    int try_readlock(void);    int try_writelock(void);    //... }; 

The rw_mutex class inherits the mutex class. Figure 11-3 shows the class relationships between our rw_mutex class, mutex class, synchronization_variable class and our runtime_error class.

Figure 11-3. The class relationships between rw_mutex , mutex , synchronization_variable , and runtime_error classes.

graphics/11fig03.gif

So far we have a somewhat narrow interface. We are only interested in providing the core minimum set of attributes and characteristics needed to generalize our mutex class using the mutex types and functions from the Pthread library. However, once we are done with this narrow interface for this mutex class we use that interface as a basis for our semi-fat interface. The narrow interface typically is used with classes that are all related through inheritance in some way. The fat interfaces tend to be used with classes that are related by functionality and not by inheritance. We can use the interface class to simplify the interface on classes or functions that belong to different libraries but have similar functionality. The interface class will provide the programmer with a consistent look and feel. We take each of the libraries or classes that have similar functionality, collect all of the common functions and variables, then generalize that functionality into a large class that contains all of the required functions and attributes. This will define a class with a fat interface. However, if we just include the functions and data we are interested in, we (e.g., rw_mutex class) then have a semi-fat interface. It has some of the advantages of the fat interface by allowing us to access objects that are only related by functionality and it restricts the set of methods the programmer has to deal with to the methods contained in the narrow interface class. This can be very important when integrating large function libraries like MPI and PVM with the POSIX facilities for concurrency. The combination of the MPI, PVM, and POSIX facilities represents hundreds of functions that all have very similar goals. Taking the time to streamline this functionality into interface classes will allow the programmer to reduce some of the complexity involved with parallel and distributed programming. Also, these interface classes become reusable components that support concurrency.

To see how we approach our semi-fat interface, lets provide an interface class for the POSIX semaphore. Although the semaphore is not part of the Pthread library, it certainly has similar uses within a multithreaded environment. However, it can be used in an environment that includes concurrently executing processes as well as threads. So in some cases it is a more general synchronization variable than our mutex class.

We might define our semaphore class in Example 11.10 as:

Example 11.10 Declaration of semaphore class.
 class semaphore : public synchronization_variable{ protected:    sem_t  *Semaphore; public:    //...    int lock(void);    int unlock(void);    int trylock(void);    //... }; 

Synopsis

 <semaphore.h> int   sem_init(sem_t *, int, unsigned int); int   sem_destroy(sem_t *); sem_t *sem_open(const char *, int, ...); int   sem_close(sem_t *); int   sem_unlink(const char *); int   sem_wait(sem_t *); int   sem_trywait(sem_t *); int   sem_post(sem_t *); int   sem_getvalue(sem_t *, int *); 

Notice that it has the same interface as our mutex class. What's the difference? First there are several important POSIX semaphore functions. Although the interfaces of mutex and semaphore are the same, the implementation of the lock() , unlock() , trylock() , and so on functions will be calls to the POSIX semaphore functions. For instance:

Example 11.11 Definitions of lock() , unlock() , and trylock() methods for the semaphore class.
 int semaphore::lock(void) {    //...    return(sem_wait(Semaphore)); } int semaphore::unlock(void) {     //...     return(sem_post(Semaphore)); } 

So the lock() , unlock() , trylock() , and so on, functions will wrap POSIX semaphore functions instead of Pthread functions. It is very important to note that a semaphore and a mutex are not the same thing. However, they can be used in similar situations. Often from the point of view of instructions that are implementing parallelism, the lock() and the unlock() mechanisms serve the same purpose. Table 11-2 shows some of the fundamental differences between a mutex and a semaphore.

While the differences in semantics in Table 11-2 are important, they are often not enough to justify a completely different interface to semaphore and mutexes. Therefore, we keep our lock() , unlock() , and trylock() semi-fat interface with the caveat that the programmer must know the differences between a mutex and a semaphore. This is similar to the situation that arises with the fat interfaces of the container classes such as deque, queue, set, multiset, and so on. The container classes are related by interface but their semantics are different in certain areas. Using the notion of an interface class, synchronization components can be designed for mutexes, condition variables, read/write mutexes, and semaphores. Once we have these components, we can design concurrency-safe container classes, domain classes, and framework classes. We can also use the interface classes to provide a single interface to different versions of the same function library, where both versions need to be used within the same application for some reason. Sometimes the interface class can be used to bridge the gap between deprecated, obsolete functions and new functionality. We often want to insulate the application programmer from the difference between operating systems. When the System V semaphores or POSIX semaphores are used, the programmer can be provided with a consistent API using an interface class.

Table 11-2. Fundamental Differences between Mutexes and Semaphores

Characteristics of Mutexes

Characteristics of Semaphores

Mutexes and condition variables are shared between threads.

Semaphores are typically shared between processes, but may also be shared between threads.

A mutex is unlocked by the same threads that locked it.

A semaphore post can be performed by other than the original thread or process that held it.

A mutex is either locked or unlocked.

A semaphore is managed by its reference count state.

 

The POSIX standard includes named semaphores.



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