Thread Synchronization - Read and Write Locks

Reader-writer lock

Reader-writer locks are similar to mutexes, but read-write locks allow higher parallelism.Mutexes are either locked or unlocked and can be locked by only one thread at a time.Read and write locks can have three states: read mode locked state, write mode locked state and unlocked state.Only one thread at a time can hold read-write locks in write mode, but multiple threads can hold read-write locks in read mode at the same time.

When a read-write lock is write-locked, all threads attempting to lock the lock will be blocked until the lock is released.When a read-write lock is in a Read-Lock state, all threads attempting to lock it in read mode can gain access, but any thread wishing to lock it in write mode will block until all threads release their read locks.Although the implementation of read-write locks varies from operating system to operating system, when a read-write lock is locked in read mode and a thread attempts to acquire a lock in write mode, read-write locks usually block subsequent read-mode lock requests.This allows read-mode locks to be held for a long time, while waiting write-mode lock requests remain unsatisfactory.

Read-write lock usage

Read-write locks are ideal for situations where data is read much more than written.When a read-write lock is locked in write mode, it says the protected data can be safely modified because only one thread at a time can have a read-write lock in write mode.When a read-write lock is locked in read mode, as long as the thread acquires a read-write lock in read mode first, the data protected by the lock can be read by multiple threads that acquire a read-mode lock.

A read-write lock is also called a shared-exclusive lock.When a read-write lock is locked in read mode, it can be said that it is locked in shared mode.When it is locked in write mode, it can be said to be locked in mutually exclusive mode.

Read-Write Lock API Function

//header file
#include <pthread.h>
#Include <time.h> //struct timespec structure

//Read-Write Lock API Function
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);  //Write Mode Lock Function
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);  //Read Mode Lock Function
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);  //Unlock Read-Write Function
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_timedrdlock(pthread_rwlock_t *restrict rwlock, const struct timespec *restrict abs_timeout);
int pthread_rwlock_timedwrlock(pthread_rwlock_t *restrict rwlock, const struct timespec *restrict abs_timeout);

Read-write lock initialization/destruction

Like mutexes, read and write locks must be initialized before they can be used and destroyed before their underlying memory can be freed.

#include <pthread.h>
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
[Parameters]
rwlock: Read and write lock pointer.
attr: Read and write lock properties.If the parameter value is NULL,Represents the creation of a read-write lock for a default property.
[Description) pthread_rwlock_init The function dynamically creates a read-write lock and initializes it.

int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
[Description) Destroy rwlock Read-write lock that the pointer points to.
[Return Value) The return value of two functions: success, return 0; failure, return error code.

[Extension) Initialization method for statically assigned read and write locks
pthread_rwlock_t rwlock=PTHREAD_RWLOCK_INITIALIZER;

[Explanation] Before using the memory occupied by read-write locks, the pthread_rwlock_destroy function needs to be called to clean up.If the pthread_rwlock_init function allocates resources for read-write locks, the pthread_rwlock_destroy function releases those resources.If the memory space occupied by a read-write lock is freed before pthread_rwlock_destroy is called, the resources allocating the lock will be lost.

Read-Write Lock Lock/Lock

Locking a read-write lock in read mode requires calling the pthread_rwlock_rdlock function.To lock a read-write lock in write mode, you need to call the pthread_rwlock_wrlock function.Regardless of how the read and write locks are locked, the pthread_rwlock_unlock function can be called to unlock them.

#include <pthread.h>
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);  //Write Mode Lock Function
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);  //Read Mode Lock Function
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);  //Unlock Read-Write Function

[Return Value) The return value of all functions: success, return 0; failure, return error code.

The Single UNIX Specification also defines a conditional version of the read-write lock primitive.

#include <pthread.h>
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);

[Return Value) The return value of two functions: success, return 0; failure, return error code.
[Description) These two functions are conditional versions of read and write locks. When a lock can be acquired, the return value is 0. When a lock cannot be acquired, the thread does not block, but returns directly EBUSY Error code.

 

Read-write lock with timeout

As with mutexes, Single UNIX Specification provides read-write lock locking functions with timeouts to prevent applications from getting permanently blocked when acquiring read-write locks.

#include <pthread.h>
#include <time.h>
int pthread_rwlock_timedrdlock(pthread_rwlock_t *restrict rwlock, const struct timespec *restrict abs_timeout);
int pthread_rwlock_timedwrlock(pthread_rwlock_t *restrict rwlock, const struct timespec *restrict abs_timeout);

The return value of two functions: success, return 0; failure, return error code.
The behavior of these two functions is similar to their timeless version.The abs_timeout parameter points to the struct timespec structure, specifying when the thread should be blocked.If a thread cannot acquire a read-write lock, the timeout expires.
Both functions will return an ETIMEDOUT error.Similar to the pthread_mutex_timedlock function, timeouts specify absolute time, not relative time.

Example 1: How to use read-write locks.In this instance, multiple worker threads get jobs assigned to them by a single main thread, and the job request queue is protected by read-write locks.

  1 #include <stdlib.h>
  2 #include <pthread.h>
  3 
  4 struct job {
  5     struct job *j_next;
  6     struct job *j_prev;
  7     pthread_t   j_id;   /* tells which thread handles this job */
  8     /* ... more stuff here ... */
  9 };
 10 
 11 struct queue {
 12     struct job      *q_head;
 13     struct job      *q_tail;
 14     pthread_rwlock_t q_lock;
 15 };
 16 
 17 /*
 18  * Initialize a queue.
 19  */
 20 int
 21 queue_init(struct queue *qp)
 22 {
 23     int err;
 24 
 25     qp->q_head = NULL;
 26     qp->q_tail = NULL;
 27     //Initialize read-write lock
 28     err = pthread_rwlock_init(&qp->q_lock, NULL);
 29     if (err != 0)
 30         return(err);
 31     /* ... continue initialization ... */
 32     return(0);
 33 }
 34 
 35 /*
 36  * Insert a job at the head of the queue.
 37  */
 38 void
 39 job_insert(struct queue *qp, struct job *jp)
 40 {
 41     pthread_rwlock_wrlock(&qp->q_lock); //Write and read locks
 42     jp->j_next = qp->q_head; //Job to be inserted j_next Pointer points to the first in the current queue job node
 43     jp->j_prev = NULL;
 44     if (qp->q_head != NULL) //If the queue is not empty
 45         qp->q_head->j_prev = jp; //Current Queue Head Node j_prev Pointer to Insert job node
 46     else //If the queue is empty
 47         qp->q_tail = jp; //Queue End Pointer q_tail Also pointing to the new job node
 48     qp->q_head = jp; //Queue Header Pointer q_head Point to newly inserted job Node, New job Nodes are inserted in the queue header
 49     pthread_rwlock_unlock(&qp->q_lock); //Unlock read-write locks
 50 }
 51 
 52 /*
 53  * Append a job on the tail of the queue.
 54  */
 55 void
 56 job_append(struct queue *qp, struct job *jp)
 57 {
 58     pthread_rwlock_wrlock(&qp->q_lock); //Write and read locks
 59     jp->j_next = NULL;
 60     jp->j_prev = qp->q_tail; //Queue End Pointer Points to the Forward Node of the Current End Node
 61     if (qp->q_tail != NULL)
 62         qp->q_tail->j_next = jp; //At the end of the current queue j_next Point to New Node jp
 63     else
 64         qp->q_head = jp;    /* list was empty */
 65     qp->q_tail = jp; //Queue End Pointer Points to New Node
 66     pthread_rwlock_unlock(&qp->q_lock);
 67 }
 68 
 69 /*
 70  * Remove the given job from a queue.
 71  */
 72 void
 73 job_remove(struct queue *qp, struct job *jp)
 74 {
 75     pthread_rwlock_wrlock(&qp->q_lock);
 76     if (jp == qp->q_head) { //Nodes to be deleted jp Is Queue Head Node
 77         qp->q_head = jp->j_next; //Queue Header Pointer Pointing jp Successor nodes
 78         if (qp->q_tail == jp)
 79             qp->q_tail = NULL;
 80         else
 81             jp->j_next->j_prev = jp->j_prev; //jp Succeeding node j_prev point jp Forward Node
 82     } else if (jp == qp->q_tail) { //Nodes to be deleted jp Is End of Queue
 83         qp->q_tail = jp->j_prev; //Queue tail pointer to jp Forward Node
 84         jp->j_prev->j_next = jp->j_next; //jp Predecessor node j_next point jp Successor Nodes
 85     } else {
 86         jp->j_prev->j_next = jp->j_next; //jp Predecessor node j_next point jp Successor nodes
 87         jp->j_next->j_prev = jp->j_prev; //jp Succeeding node j_prev point jp Forward Node
 88     }
 89     pthread_rwlock_unlock(&qp->q_lock);
 90 }
 91 
 92 /*
 93  * Find a job for the given thread ID.
 94  */
 95 struct job *
 96 job_find(struct queue *qp, pthread_t id)
 97 {
 98     struct job *jp;
 99 
100     if (pthread_rwlock_rdlock(&qp->q_lock) != 0) //Read and write lock
101         return(NULL);
102 
103     for (jp = qp->q_head; jp != NULL; jp = jp->j_next)
104         if (pthread_equal(jp->j_id, id))
105             break;
106 
107     pthread_rwlock_unlock(&qp->q_lock);
108     return(jp);
109 }
rwlock.c

[Analysis]

1. Write mode is used to lock read and write locks on queues when jobs are added to or removed from the queue.

2. When searching queues, read mode is used to lock the read and write locks of the queues, allowing all worker threads to search the queues concurrently.

3. Work threads can only read jobs from queues that match their thread ID s.Since the job structure can only be used by one thread at a time, no additional locks are required.

4. Use read-write locks to improve performance only if the threads search for jobs in the queue much more frequently than increase or delete jobs.

Tags: PHP Unix

Posted on Mon, 05 Aug 2019 11:45:05 -0700 by SkyRanger