Saturday, October 25, 2014

Unix Prog: Reader-Writer Lock

1. Reader-Writer Locks

Three states for reader_writer locks:
1) locks in read mode
2) locks in write mode
3) unlocked mode

Only one thread can be in lock-in-write mode, but multiple threads can coexist i n lock-in-read mode.

1) If the lock is write-locked, no threads can gain the lock until it is released
2) If the lock is read-locked, threads attempting to lock it in read mode are given access, but threads attempting to lock it in write mode will be blocked.
3) read-writer usually blocks additional readers if a lock is already held in read mode and a thread is blocked trying to acquire the lock in write mode. This prevents a constant stream of readers from starving waiting writers.

Read-writer locks are well suited for situations in which data structures are read more often than they are modified.

2. System Call Definitions:
read-writer locks must be initialized before use and destroyed before freeing their underlying memory:

 ubuntu@ip-172-31-23-227:~$ less /usr/include/pthread.h  
 ......  
 /* Initialize read-write lock RWLOCK using attributes ATTR, or use  
   the default values if later is NULL. */  
 extern int pthread_rwlock_init (pthread_rwlock_t *__restrict __rwlock,  
                 const pthread_rwlockattr_t *__restrict  
                 __attr) __THROW __nonnull ((1));  
   
 /* Destroy read-write lock RWLOCK. */  
 extern int pthread_rwlock_destroy (pthread_rwlock_t *__rwlock)  
    __THROW __nonnull ((1));  
   
 /* Acquire read lock for RWLOCK. */  
 extern int pthread_rwlock_rdlock (pthread_rwlock_t *__rwlock)  
    __THROWNL __nonnull ((1));  
   
 /* Try to acquire read lock for RWLOCK. */  
 extern int pthread_rwlock_tryrdlock (pthread_rwlock_t *__rwlock)  
  __THROWNL __nonnull ((1));  
   
 /* Acquire write lock for RWLOCK. */  
 extern int pthread_rwlock_wrlock (pthread_rwlock_t *__rwlock)  
    __THROWNL __nonnull ((1));  
   
 /* Try to acquire write lock for RWLOCK. */  
 extern int pthread_rwlock_trywrlock (pthread_rwlock_t *__rwlock)  
    __THROWNL __nonnull ((1));  
   
 /* Unlock RWLOCK. */  
 extern int pthread_rwlock_unlock (pthread_rwlock_t *__rwlock)  
    __THROWNL __nonnull ((1));  
 ......  

To lock a reader-writer lock in read mode, we call pthread_rwlock_rdlock.
To write-lock mode, we call pthread_rwlock_wrlock
To unlock, we call pthread_rwlock_unlock

Similar from pthread_mutex_trylock, pthread_rwlock_tryrdlock and pthread_rwlock_trywrlock,  they never blocked, but if the lock is not locked, it will lock it.

trylock is very useful for avoding the deadlock, since it never block.

2. Example:
rwlock.c:
 #include<stdio.h>  
 #include<stdlib.h>  
 #include<unistd.h>  
 #include<pthread.h>  
   
 struct job {  
  struct job *j_next;  
  struct job *j_prev;  
  pthread_t j_id;  
 };  
   
 struct queue {  
  struct job *q_head;  
  struct job *q_tail;  
  pthread_rwlock_t q_lock;  
 };  
   
 // Initialize the queue by setting up the head, tail pointer  
 // And also init the reader-writer lock  
 int queue_init(struct queue *qp)  
 {  
  int err;  
  qp->q_head = NULL;  
  qp->q_tail = NULL;  
  err = pthread_rwlock_init(&qp->q_lock, NULL);  
  if(err != 0)  
   return err;  
   
  return 0;  
 }  
   
 void job_insert(struct queue *qp, struct job *jp)  
 {  
  // Lock into write-mode  
  pthread_rwlock_wrlock(&qp->q_lock);  
   
  // Insert the job in front of the head of queue  
  jp->j_next = qp->q_head;  
  jp->j_prev = NULL;  
   
  if(qp->q_head != NULL)  
   qp->q_head->j_prev = jp;  
  else  
   qp->q_tail = jp;  
  qp->q_head = jp;  
   
  // Unlock  
  pthread_rwlock_unlock(&qp->q_lock);  
 }  
   
 void job_append(struct queue *qp, struct job *jp)  
 {  
  // Lock into write-mode  
  pthread_rwlock_wrlock(&qp->q_lock);  
   
  // Append the job to the tail of queue  
  jp->j_next = NULL;  
  jp->j_prev = qp->q_tail;  
  if(qp->q_tail != NULL)  
   qp->q_tail->j_next = jp;  
  else  
   qp->q_head = jp;  
  qp->q_tail = jp;  
   
  // Unlock  
  pthread_rwlock_unlock(&qp->q_lock);  
 }  
   
 void job_remove(struct queue *qp, struct job *jp)  
 {  
  // Lock into the write mode  
  pthread_rwlock_wrlock(&qp->q_lock);  
   
  // remove the job from the queue  
  if(jp == qp->q_head) {  
   qp->q_head = jp->j_next;  
   if(qp->q_tail == jp)  
    qp->q_tail = NULL;  
   else  
    jp->j_next->j_prev = jp->j_prev;  
  }  
  else if(jp == qp->q_tail) {  
   qp->q_tail = jp->j_prev;  
   if(qp->q_head == jp)  
    qp->q_head = NULL;  
   else  
    jp->j_prev->j_next = jp->j_next;  
  }  
  else {  
   jp->j_prev->j_next = jp->j_next;  
   jp->j_next->j_prev = jp->j_prev;  
  }  
   
  // Unlock  
  pthread_rwlock_unlock(&qp->q_lock);  
 }  
   
 struct job *job_find(struct queue *qp, pthread_t id)  
 {  
  struct job *jp;  
   
  if(pthread_rwlock_rdlock(&qp->q_lock) != 0)  
   return NULL;  
   
  for(jp = qp->q_head; jp != NULL; jp = jp->j_next)  
   if(pthread_equal(jp->j_id, id))  
    break;  
   
  pthread_rwlock_unlock(&qp->q_lock);  
  return jp;  
 }  
   
 int main(int argc, char* argv[])  
 {  
  struct queue que;  
  struct job job;  
   
  queue_init(&que);  
  job_insert(&que, &job);  
  job_remove(&que, &job);  
   
  exit(0);  
 }  

Every time when the thread operate the global struct queue, we use read-write lock to lock. In this way, we make global struct queue to be thread safe.

No comments:

Post a Comment