ยง31.1โ€“31.3Semaphores: A Definition โ€ฆ Semaphores For Ordering

Part II OSTEP pp. 367โ€“371 ยท ~8 min read

  • semaphore
  • binary semaphore

We now have locks and condition variables โ€” but Dijkstra realized years ago that a single primitive could do both. That primitive is the semaphore .

The Crux: How To Use Semaphores

How can we use semaphores instead of locks and condition variables? What is a semaphore, and a binary semaphore? Can we build a semaphore from locks and CVs โ€” and locks and CVs from semaphores?

31.1 Semaphores: A Definition

A semaphore is an integer value manipulated by two atomic routines. In POSIX theyโ€™re sem_wait() and sem_post() (Dijkstra called them P and V, from Dutch; some say down and up). You must initialize it first โ€” and the initial value is everything:

#include <semaphore.h>
sem_t s;
sem_init(&s, 0, 1); // third arg is the initial value; 0 = shared within a process

The behavior of the two operations:

int sem_wait(sem_t *s) {
    decrement the value of s by one
    wait if the value of s is negative
}
int sem_post(sem_t *s) {
    increment the value of s by one
    if there are one or more threads waiting, wake one
}

Three things to internalize: sem_wait() either returns immediately (if the value was โ‰ฅ 1) or suspends the caller until a later post; sem_post() never waits โ€” it just increments and wakes one waiter if any; and, by Dijkstraโ€™s invariant, when the value is negative, its magnitude equals the number of waiting threads. Step through it:

A semaphore is just an integer + a wait queue. Watch the value โ€” and the invariant that a negative value counts the sleepers.
semaphore value1wait queue:(empty)value negative โ†’ |value| = number of sleeping waiters

1sem_init(&s, 0, 1)

Initialize to 1. Think of the value as "resources available to give away right now" โ€” here, one.

step 1 / 6

(Assume for now these actions happen atomically โ€” ยง31.8 builds a semaphore from a lock and a CV to make that true.)

31.2 Binary Semaphores (Locks)

Our first use is a familiar one: a lock. Bracket the critical section with a wait/post pair โ€” and initialize to 1:

sem_t m;
sem_init(&m, 0, 1); // X = 1

sem_wait(&m);
// critical section here
sem_post(&m);

Why 1? The first threadโ€™s sem_wait drives the value 1 โ†’ 0 and enters; a second threadโ€™s sem_wait drives it 0 โ†’ โˆ’1 and sleeps, until the holder posts. Watch two threads contend:

Figure 31.5: two threads sharing a semaphore-as-lock (init 1). Follow the Value row: 1 โ†’ 0 โ†’ โˆ’1 โ†’ 0 โ†’ 1.
t =
Thread 0
Thread 1
semaphore
value 0
tick 1 / 6 ยท one scheduling step per tickrunningreadysleeping

T0 calls sem_wait: value 1 โ†’ 0. Since 0 โ‰ฅ 0 it does NOT wait โ€” it enters the critical section.

Because a lock has only two states (held / not held), a semaphore used this way is a binary semaphore . (Used purely as a binary lock, it could be implemented more simply than the general semaphore.)

31.3 Semaphores For Ordering

Semaphores also order events: one thread waits for something another thread makes happen โ€” exactly what condition variables did. The classic example is a parent waiting for a child (init to 0 this time):

sem_t s;
void *child(void *arg) {
    printf("child\n");
    sem_post(&s); // signal: child is done
    return NULL;
}
int main() {
    sem_init(&s, 0, 0); // X = 0
    printf("parent: begin\n");
    pthread_t c;
    Pthread_create(&c, NULL, child, NULL);
    sem_wait(&s); // wait for the child
    printf("parent: end\n");
}

Why 0? If the parent reaches sem_wait first, the value goes 0 โ†’ โˆ’1 and it sleeps, exactly as we want:

Figure 31.7: a parent waiting for its child, semaphore init to 0. Value: 0 โ†’ โˆ’1 โ†’ 0.
t =
Parent
Child
semaphore
value โˆ’1
tick 1 / 4 ยท one scheduling step per tickrunningreadysleeping

The parent creates the child, then calls sem_wait: value 0 โ†’ โˆ’1, so it sleeps. Init 0 means "nothing to give away yet."

And the other order works too (Case 2): if the child runs to completion first, its sem_post drives the value 0 โ†’ 1; when the parent finally calls sem_wait, it finds 1, decrements to 0, and returns without blocking. Either way, โ€œchildโ€ prints before โ€œparent: endโ€.

Aside: Setting The Initial Value

Two examples, two values โ€” 1 for a lock, 0 for ordering. The general rule (thanks to Perry Kivolowitz): the initial value is the number of resources youโ€™re willing to give away immediately after initialization. A lock hands out 1 (it may be held at once); ordering hands out 0 (the resource doesnโ€™t exist until the other thread creates it by posting).

The one decision that changes everything: the initial value. Kivolowitz's rule โ€” "how many resources can you give away immediately?"
init valuewhy
As a lock (binary semaphore)1one thread may hold the lock immediately
For ordering (wait for an event)0nothing exists yet โ€” the waiter must block until the signaler posts
Dotted-underlined cells have explanations โ€” click one.

Same primitive, two opposite jobs โ€” decided entirely by the initial value. Next: the producer/consumer problem, rebuilt on semaphores.

Check yourself: semaphores, locks, and ordering

1.A semaphore's value is currently 1. A thread calls sem_wait(). What happens?

2.You observe a semaphore's value is โˆ’3. What does that tell you?

3.To use a semaphore as a lock (a binary semaphore), what initial value do you pass, and why?

4.For ordering (a parent waiting for a child), the semaphore is initialized to 0. Why 0?

5.Does sem_post() ever block the calling thread?

6.Ordering, Case 2: the child runs to completion BEFORE the parent reaches sem_wait(). What happens?

6 questions