We keep track of a boolean variable for "writer active," since there can only be one. There are also counters for "readers active," "readers waiting," and "writers waiting." We could get by without counters for readers and writers waiting. All readers are awakened simultaneously using a broadcast, so it doesn't matter how many there are. Writers are awakened only if there are no readers, so we could dispense with keeping track of whether there are any threads waiting to write (at the cost of an occasional wasted condition variable signal when there are no waiters).
We count the number of threads waiting for read access because the condition variable waits might be canceled. Without cancellation, we could use a simple flag — "threads are waiting for read" or "no threads are waiting for read." Each thread could set it before waiting, and we could clear it before broadcasting to wake all waiting readers. However, because we can't count the threads waiting on a condition variable, we wouldn't know whether to clear that flag when a waiting reader was canceled. This information is critical, because if there are no readers waiting when the read/write lock is unlocked, we must wake a writer — but we cannot wake a writer if there are waiting readers. A count of waiting readers, which we can decrease when a waiter is canceled, solves the problem.
The consequences of "getting it wrong" are less important for writers than for readers. Because we check for readers first, we don't really need to know whether there are writers. We could signal a "potential writer" anytime the read/write lock was released with no waiting readers. But counting waiting writers allows us to avoid a condition variable signal when no threads are waiting.
Part 2 shows the rest of the definitions and the function prototypes.
4-6 The RWLOCK_INITIALlZER macro allows you to statically initialize a read/write lock.
11-18 Of course, you must also be able to initialize a read/write lock that you cannot allocate statically, so we provide rwl_init
to initialize dynamically, and rwl_ destroy to destroy a read/write lock once you're done with it. In addition, there are functions to lock and unlock the read/write lock for either read or write access. You can "try to lock" a read/write lock, either for read or write access, by calling rwl_readtrylock
or rwl_writetrylock., just as you can try to lock a mutex by calling pthread_mutex_trylock
.
■ rwlock.h part 2 interfaces
1 /*
2 * Support static initialization of barriers.
3 */
4 #define RWL_INITIALIZER \
5 {PTHREAD_MUTEX_INITIALIZER, PTHREAD_COND_INITIALIZER, \
6 PTHREAD_COND_INITIALIZER, RWLOCK_VALID, 0, 0, 0, 0}
7
8 /*
9 * Define read/write lock functions.
10 */
11 extern int rwl_init (rwlock_t *rwlock);
12 extern int rwl_destroy (rwlock_t *rwlock);
13 extern int rwl_readlock (rwlock_t *rwlock);
14 extern int rwl_readtrylock (rwlock_t *rwlock);
15 extern int rwl_readunlock (rwlock_t *rwlock);
16 extern int rwl_writelock (rwlock_t *rwlock);
17 extern int rwl_writetrylock (rwlock_t *rwlock);
18 extern int rwl_writeunlock (rwlock_t *rwlock);
The file rwlock.c contains the implementation of read/write locks. The following examples break down each of the functions used to implement the rwlock.h interfaces.
Part 1 shows rwl_init
, which initializes a read/write lock. It initializes the Pthreads synchronization objects, initializes the counters and flags, and finally sets the valid sentinel to make the read/write lock recognizable to the other interfaces. If we are unable to initialize the read condition variable, we destroy the mutex that we'd already created. Similarly, if we are unable to initialize the write condition variable, we destroy both the mutex and the read condition variable.
■ rwlock.c part 1 rwl_init
1 #include
2 #include "errors.h"
3 #include "rwlock.h"
4
5 /*
6 * Initialize a read/write lock.
7 */
8 int rwl_init (rwlock_t *rwl)
9 {
10 int status;
11
12 rwl->r_active = 0;
13 rwl->r_wait = rwl->w_wait = 0;
14 rwl->w_active = 0;
15 status = pthread_mutex_init (&rwl->mutex, NULL);
16 if (status != 0)