Β§31.5–31.8Reader-Writer Locks … Summary

Part II OSTEP pp. 376–382 Β· ~8 min read

  • reader-writer lock
  • dining philosophers
  • thread throttling

We close the semaphore tour with a flexible lock, the most famous concurrency puzzle of all, and a look under the hood.

31.5 Reader-Writer Locks

Sometimes one lock is too blunt: many operations only read a structure and could safely run together; only writes need exclusivity. A reader-writer lock captures that β€” many readers OR one writer:

typedef struct _rwlock_t {
    sem_t lock;      // binary semaphore, guards `readers`
    sem_t writelock; // held while ANY reader or the writer is active
    int   readers;   // # readers currently inside
} rwlock_t;

void rwlock_acquire_readlock(rwlock_t *rw) {
    sem_wait(&rw->lock);
    rw->readers++;
    if (rw->readers == 1)          // FIRST reader...
        sem_wait(&rw->writelock);  // ...grabs the writelock
    sem_post(&rw->lock);
}
void rwlock_release_readlock(rwlock_t *rw) {
    sem_wait(&rw->lock);
    rw->readers--;
    if (rw->readers == 0)          // LAST reader...
        sem_post(&rw->writelock);  // ...releases it
    sem_post(&rw->lock);
}
void rwlock_acquire_writelock(rwlock_t *rw) { sem_wait(&rw->writelock); }
void rwlock_release_writelock(rwlock_t *rw) { sem_post(&rw->writelock); }

The clever bit is the first/last reader dance with writelock:

A reader-writer lock: many readers share, one writer excludes. The first reader grabs the writelock; the last reader releases it.
readers inside0writelock πŸ”’FREEWriter W: none

1Empty

Nobody in the critical section: readers = 0, writelock free.

step 1 / 6

It works, but note the fairness flaw the trace ends on: as long as readers keep arriving, readers never hits 0, so a waiting writer can starve. (A better version blocks new readers once a writer is waiting.) And reader-writer locks often aren’t worth it β€” the extra overhead can outweigh the concurrency gained.

Tip: Simple And Dumb Can Be Better (Hill’s Law)

Never underestimate the simple, dumb approach. A plain spin lock is easy to build and fast; reader-writer locks sound cool but are complex, and complex often means slow. Mark Hill found simple direct-mapped caches beat fancy set-associative ones β€” β€œbig and dumb is better.” Try the simple approach first.

31.6 The Dining Philosophers

Dijkstra’s famous puzzle β€” the dining philosophers β€” impractical, but a rite of passage (and a favorite interview question). Five philosophers, five forks; each alternates thinking and eating, and needs both neighboring forks to eat. The loop:

while (1) {
    think();
    get_forks(p);
    eat();
    put_forks(p);
}
int left(int p)  { return p; }
int right(int p) { return (p + 1) % 5; }

The naive get_forks β€” grab left, then right β€” deadlocks. Walk it:

The dining philosophers: 5 thinkers, 5 forks, each needs both neighbors'. Watch the naive solution deadlock β€” then break the cycle.
tablef0f1f2f3f4P0thinkP1thinkP2thinkP3thinkP4think

1Everyone thinks

Five philosophers sit around a table with one fork between each pair. To eat, a philosopher needs BOTH neighboring forks: left(p) = fork p, right(p) = fork (p+1) % 5.

step 1 / 4

Figure 31.15: broken (deadlocks)

void get_forks(int p) {
    sem_wait(&forks[left(p)]);
    sem_wait(&forks[right(p)]);
}

Figure 31.16: the fix

void get_forks(int p) {
    if (p == 4) { // highest grabs right first
        sem_wait(&forks[right(p)]);
        sem_wait(&forks[left(p)]);
    } else {
        sem_wait(&forks[left(p)]);
        sem_wait(&forks[right(p)]);
    }
}

Changing the acquisition order for just one philosopher breaks the circular dependency β€” no cycle can form, so no deadlock. (put_forks stays the same.) We’ll see this β€œbreak the cycle” idea generalized in the next chapter on deadlock.

31.7 Thread Throttling

A humbler use: throttling (admission control). To keep β€œtoo many” threads out of a dangerous region β€” say a memory-hungry section that would exhaust RAM and trigger thrashing if all threads entered at once β€” initialize a semaphore to the maximum you’ll allow, and bracket the region with sem_wait()/sem_post(). The semaphore naturally caps concurrency at that number.

31.8 How To Implement Semaphores

Finally, we can build a semaphore from the primitives it generalizes β€” one lock, one condition variable, and a value (the authors dub it a β€œZemaphore”):

typedef struct __Zem_t {
    int value;
    pthread_cond_t cond;
    pthread_mutex_t lock;
} Zem_t;

void Zem_wait(Zem_t *s) {
    Mutex_lock(&s->lock);
    while (s->value <= 0)
        Cond_wait(&s->cond, &s->lock);
    s->value--;
    Mutex_unlock(&s->lock);
}
void Zem_post(Zem_t *s) {
    Mutex_lock(&s->lock);
    s->value++;
    Cond_signal(&s->cond);
    Mutex_unlock(&s->lock);
}

One deliberate difference from Dijkstra’s semaphore: the Zemaphore’s value never goes below 0, so it doesn’t track the waiter count β€” this is simpler and matches Linux. Curiously, the reverse β€” building a condition variable out of semaphores β€” is famously hard; even expert concurrent programmers got it wrong.

Tip: Be Careful With Generalization

Generalization is powerful β€” one good idea broadened to solve a larger class of problems. But as Lampson warned, β€œDon’t generalize; generalizations are generally wrong.” Semaphores generalize locks and CVs β€” yet given how hard a CV-on-semaphores is, maybe the generalization isn’t as general as it looks.

31.9 Summary

Semaphores are a powerful, flexible primitive β€” some programmers use them for everything, shunning locks and CVs. We’ve seen just a few classics (fork/join, bounded buffer, reader-writer, dining philosophers, throttling); Downey’s free Little Book of Semaphores has many more.

Check yourself: reader-writer, philosophers, and building semaphores

1.In the reader-writer lock, when does a reader touch the writelock semaphore?

2.What fairness problem does this reader-writer lock have?

3.Why does the naive dining-philosophers solution (each grabs left then right) deadlock?

4.How does Dijkstra's fix (philosopher 4 grabs right-then-left) prevent the deadlock?

5.You want to prevent more than 3 threads from entering a memory-hungry region at once. How?

6.The Zemaphore builds a semaphore from a lock + condition variable + value. What's the notable difference from Dijkstra's semaphore?

6 questions