Add Book to My BookshelfPurchase This Book Online

Chapter 3 - Synchronizing Pthreads

Pthreads Programming
Bradford Nichols, Dick Buttlar and Jacqueline Proulx Farrell
 Copyright © 1996 O'Reilly & Associates, Inc.

Reader/Writer Locks
This next example can be a mind-bender if you're not used to synchronization. It takes two synchronization primitives, the mutex and the condition variable, and creates a third—the reader/writer lock.
Let's review the reasons for reader/writer locks and the rules by which they operate. If a thread tries to get a read lock on a resource, it will succeed only if no other thread holds a lock on the resource or if all threads that hold a lock are readers. If another thread holds a write lock on the resource, the would-be reader must wait. Conversely, if a thread tries to get a write lock on the resource, it must wait if any other thread holds a read or write lock.
We'll start by defining a reader/writer variable of type pthread_rdwr_tand by creating the functions that operate on it, as listed in Table 3-1*
 *By convention, functions that extend the Pthreads standard should start with pthread and end with np  (for nonportable). We'll follow this convention in this section.
Table 3-1: Reader/Writer Lock Functions
Function
Description
pthread_rdwr_init_np
Initialize reader/writer lock
pthread_rdwr_rlock_np
Obtain read lock
pthread_rdwr_wlock_np
Obtain write lock
pthread_rdwr_runlock_np
Release read lock
pthread_rdwr_wunlock_np
Release write lock
How could threads use this type of lock? We'll modify our linked list program in Example 3-5 to provide Example 3-8.
Example 3-8: Using Reader/Writer Locks (llist_threads_rw.c)
typedef struct llist{
   llist_node_t *first;
   pthread_rdwr_t rwlock;
   } llist_t;
.
.
.
int llist_init(llist_t *llistp)
{
  llistp->first = NULL;
  pthread_rdwr_init_np(&(llistp->rwlock), NULL);
  return 0;
}
int llist_insert_data(int index; void *datap, llist_t *llistp)
{
   llist_node_t *cur, *prev, *new;
   int found = FALSE;
   pthread_rdwr_wlock_np(&(llistp->rwlock));
   for (cur = prev = llistp->first; cur != NULL; prev = cur, cur= cur->nextp) {
            if (cur->index == index) {
                      free(cur->datap);
                      .
                      .
                      .
   pthread_rdwr_wunlock_np(&(llistp->rwlock));
   .
   .
   .
}
int llist_find_data(int index; void **datapp, llist_t *llistp)
{
   llist_node_t *cur, *prev, *new;
   int found = FALSE;
   pthread_rdwr_rlock_np(&(*llistp->rwlock));
   for (cur = prev = *llistp->first; cur != NULL;
                             prev = cur, cur= cur->nextp) {
          if (cur->index == index) {
                             free(cur->datap);
                             .
                             .
                             .
   pthread_rdwr_runlock_np(&(*llistp->rwlock));
   .
   .
   .
}
In Example 3-8, our linked list is protected by a reader/writer lock instead of a mutex. The llist_insert_data routine obtains a write lock before it modifies the list. The llist_find_data routine needs only a read lock.
We show the include file in Example 3-9. We'll define a pthread_rdwr_t structure that includes a count of readers, a count of writers, a mutex, and a condition variable. The mutex protects the reader/writer counts in the structure. Threads will wait on the condition variable for a currently held lock to become free. We've effectively hidden both mutex and condition variable in the structure so their use will be transparent to end users of the pthread_rdwr_t structure and our reader/writer functions.
Example 3-9: Include File for Reader/Writer Locks (rdwr.h)
#include <pthread.h>
typedef struct rdwr_var {
        int readers_reading;
        int writer_writing;
        pthread_mutex_t mutex;
        pthread_cond_t lock_free;
} pthread_rdwr_t;
typedef void *pthread_rdwrattr_t;
#define pthread_rdwrattr_default NULL;
int pthread_rdwr_init_np(pthread_rdwr_t *rdwrp, pthread_rdwrattr_t  *attrp);
int pthread_rdwr_rlock_np(pthread_rdwr_t *rdwrp);
int pthread_rdwr_wlock_np(pthread_rdwr_t *rdwrp);
int pthread_rdwr_runlock_np(pthread_rdwr_t *rdwrp);
int pthread_rdwr_wunlock_np(pthread_rdwr_t *rdwrp);
Because all thread objects come with attribute objects (threads, mutexes, and so on), we've defined an attribute data type for our reader/writer locks and named it pthread_rdwrattr_t. We have no use for it now, but it may come in handy someday. When we pull it out of the closet, it'll act just like the other attribute objects—it'll be initialized in a create call and come with a default value of pthread_rdwrattr_default.
The next few pages show how we've implemented the five reader/writer lock functions the threads in Example 3-8 called.
The initialization function (pthread_rdwr_init_np), in Example 3-10, simply sets the members of the rdwr variable (a pthread_rdwr_t structure) to the values they should have when the lock is not held. It initializes the mutex and condition variable to NULL. All other function calls lock the rdwr variable's mutex before proceeding to ensure that no other thread is reading or writing the variable's state at the same time.
Example 3-10: Initializing a Reader/Writer Lock (rdwr.c)
int pthread_rdwr_init_np(pthread_rdwr_t *rdwrp, pthread_rdwrattr_t  *attrp )
{
          rdwrp->readers_reading = 0;
          rdwrp->writer_writing = 0;
          pthread_mutex_init(&(rdwrp->mutex), NULL);
          pthread_cond_init(&(rdwrp->lock_free), NULL);
          return 0;
}
The get-read-lock function (pthread_rdwr_rlock_np)checks to see if another thread has a write lock on the rdwrp variable. If so, it calls the pthread_cond_wait function to wait on the lock_ free condition variable. When it is awakened and the rdwrp variable is no longer write-locked, pthread_rdwr_rlock_np increments the number of readers, releases the mutex, and returns. Note that the thread does not care about the actual value of the readers_reading member. If it were zero and the function incremented it to 1, the read lock is set and all subsequent writers must wait. If the readers_reading count were already greater than 1, the new reader would simply be added to the number of threads already reading. Example 3-11 illustrates how the function would look.
Example 3-11: Read Locking a Reader/Writer Lock (rdwr.c)
int pthread_rdwr_rlock_np(pthread_rdwr_t_np *rdwrp)
{
          pthread_mutex_lock(&(rdwrp->mutex));
          while(rdwrp->writer_writing) {
                  pthread_cond_wait(&(rdwrp->lock_free), &(rdwrp->mutex));
          }
          rdwrp->readers_reading++;
          pthread_mutex_unlock(&(rdwrp->mutex));
          return 0;
}
The get-write-lock function (pthread_rdwr_wlock_np)call, shown in Example 3-12, is similar to pthread_rdwr_rlock_np, except that it must check not only for another thread that has a write lock on the rdwrp variable, but also for any threads that have read locks. If either is TRUE, the pthread_rdwr_wlock_np function calls the pthread_cond_wait function to wait on the lock_free condition variable. When it is awakened with no readers or writers, pthread_rdwr_wlock_np sets the value of writer_writing to 1 and releases the mutex. Its caller is now—and will be—the only writer until it calls pthread_rdwr_wunlock_np to release the write lock.
Example 3-12: Write Locking a Reader/Writer Lock (rdwr.c)
int pthread_rdwr_wlock_np(pthread_rdwr_t_np *rdwrp)
{
          pthread_mutex_lock(&(rdwrp->mutex));
          while (rdwrp->writer_writing || rdwrp->readers_reading) {
                   pthread_cond_wait(&(rdwrp->lock_free), &(rdwrp->mutex));
          }
          rdwrp->writer_writing++;
          pthread_mutex_unlock(&(rdwrp->mutex));
          return 0;
}
The unlock-read-lock function (pthread_rdwr_runlock_np)reduces the count of readers for a lock, decrementing the value of the readers_reading member of the rdwrp variable. It checks the readers_reading count and, if it is zero, calls pthread_cond_signal to tell any threads waiting on the lock_free condition variable that the lock has been released and can now be locked. Like all calls that unlock resources, pthread_rdwr_runlock_np assumes that it's being used correctly—in this case, by a thread that has previously called pthread_rdwr_rlock_np.
Example 3-13: Read Unlocking a Reader/Writer Lock (rdwr.c)
int pthread_rdwr_runlock_np(pthread_rdwr_t_np *rdwrp) {
          pthread_mutex_lock(&(rdwrp->mutex));
          if (rdwrp->readers_reading == 0) {
                    pthread_mutex_unlock(&(rdwrp->mutex));
                    return -1;
          } else {
                    rdwrp->readers_reading--;
                    if (rdwrp->readers_reading == 0)
                              pthread_cond_signal(&(rdwrp->lock_free));
                    pthread_mutex_unlock(&(rdwrp->mutex));
                    return 0;
          }
}
The unlock-write-lock function (pthread_rdwr_wunlock_np) is similar to pthread_rdwr_runlock_np. Because only one writer holds the lock at a time, running this routine to release that lock should always result in a signal on the lock_freecondition as shown in Example 3-14.
Example 3-14: Write Unlocking a Reader/Writer Lock (rdwr.c)
int pthread_rdwr_wunlock_np(pthread_rdwr_t_np *rdwrp) {
          pthread_mutex_lock(&(rdwrp->mutex));
          if (rdwrp->writer_writing == 0) {
                    pthread_mutex_unlock(&(rdwrp->mutex));
                    return -1;
          } else {
                    rdwrp->writer_writing = 0;
                    pthread_cond_broadcast(&(rdwrp->lock_free));
                    pthread_mutex_unlock(&(rdwrp->mutex));
                    return 0;
          }
}
This implementation doesn't address an important issue in using reader/writer locks. If the lock is currently held by a reader and a writer is already waiting, any reader that comes along next will get the lock before the waiting writer. As long as one or more readers are waiting for the lock, regardless of when they made their requests or where in the waiting lists they're queued relative to any potential writers, the lock will continue to be held for reading. More robust implementations might suspend read lock requests that arrive after a write request is waiting and resume them when there are no more writers. The decision of how to handle incoming reads versus pending writes depends on the priorities of a given system.

Previous SectionNext Section
Books24x7.com, Inc © 2000 –  Feedback