JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

Semaphores and Counting Resources

A mutex says 'one at a time'; a condition variable says 'wait until something is true'. A semaphore folds both ideas into a single counter that tracks how many of a thing are available. This guide builds the semaphore from the ground up, shows the two ways people use it, and earns the famous one-line solution to producer-consumer.

From a lock to a counter

You arrive at this guide holding two tools. From guide 1 you have the mutex: a lock that lets exactly one thread into a critical section at a time, everyone else made to wait. From guide 2 you have the condition variable: a way to sleep until some predicate about shared state becomes true, and to be woken when another thread changes it. Both are about a yes/no fact — the lock is held or free, the predicate is true or false. The semaphore generalizes that yes/no into a count: not 'is the room free?' but 'how many seats are left?'.

Picture a small library with three identical study booths. At the door hangs a board showing how many booths are free, and beside it a bowl of three keys. A semaphore is exactly that bowl. A student who wants a booth takes a key — the count drops by one; when she leaves she returns the key — the count rises by one. If the bowl is empty when she arrives, she does not barge in or grab someone else's booth: she waits at the door until a key reappears. The whole mechanism is just an integer that you are only allowed to touch through two careful operations, and those two operations are the entire idea.

Those two operations have historical names from Dijkstra, who introduced semaphores in the mid-1960s: P (take, also called wait or down or acquire) and V (give back, also called post or signal or up or release). On modern POSIX systems the calls are sem_wait() and sem_post(), and the counter is created with sem_init(). Hold the names loosely; what matters is what each one does, which we make precise next.

The two operations, made precise

A semaphore holds a non-negative integer count, the number of units currently available. sem_wait() means 'I want one unit': if the count is greater than zero, decrement it and return immediately; if the count is zero, block — go to sleep — until someone makes it positive again, then decrement and return. sem_post() means 'I am returning one unit': increment the count, and if any threads are sleeping inside sem_wait(), wake one of them so it can finish its decrement. That is the whole contract.

  A semaphore is conceptually an int + a wait queue, with two
  ATOMIC operations.  Read this as a SKETCH of meaning, not real code:

    sem_wait(s):                 sem_post(s):
        atomically:                  atomically:
            while s.count == 0:          s.count = s.count + 1
                sleep on s                if a thread is sleeping on s:
            s.count = s.count - 1            wake exactly one of them

  Library calls (POSIX):
    sem_t s;
    sem_init(&s, 0, 3);   /* count starts at 3 ; second arg 0 = threads */
    sem_wait(&s);         /* take one : count 3 -> 2 (or block at 0)   */
    sem_post(&s);         /* give one : count 2 -> 3 (wake a waiter)   */
The meaning of the two operations, plus the real POSIX calls. The 'atomically' wrappers are doing the heavy lifting — the count and the sleep/wake are one indivisible step.

Two jobs in one tool: counting vs signalling

Semaphores get used for two genuinely different purposes, and seeing them as separate is what stops the confusion. The first is the booth story: a counting semaphore, initialized to N, that limits how many threads may hold a pooled resource at once. Say you have 4 database connections and 50 worker threads. Initialize the semaphore to 4. Each worker calls sem_wait() before using a connection and sem_post() after. At most 4 are ever inside; the other 46 sleep at the door until a connection is freed. No connection is ever double-used, and no thread spins burning CPU — they sleep until woken.

The second use is subtler and, once you see it, surprisingly powerful: a binary semaphore — one initialized to 0 or 1 — used purely as a signal between threads. Initialize it to 0, meaning 'the event has not happened yet'. Thread A calls sem_wait() and blocks immediately, because the count is 0. Later, thread B finishes some work and calls sem_post(), nudging the count to 1; that wakes A, which decrements back to 0 and proceeds. You have just made A wait for B with no busy-waiting and no shared flag to poll. This is the semaphore acting like a one-shot 'go' signal — a different shape of problem from limiting a resource pool.

Semaphore versus condition variable

Right about now a fair question surfaces: 'didn't the last guide already teach me to wait for things, with a condition variable? Why a second tool?' They overlap, but they are not the same, and the difference is memory. A condition variable has none — if thread B signals it while no thread is currently waiting, the signal is simply lost, vanishing into the air. That is why guide 2 insisted you always pair a condition variable with a real predicate over shared state and the wait-while-predicate loop: the condition variable only nudges you to re-check; the truth lives in your own variable.

A semaphore, by contrast, remembers. If B calls sem_post() before A ever calls sem_wait(), the count goes to 1 and stays there; when A eventually calls sem_wait(), it finds the 1 waiting for it and sails through without blocking. The post was not lost — it was banked in the count. That single difference is why a semaphore alone, with no extra mutex or flag, can hand a signal across threads safely, whereas a condition variable always needs a companion lock and predicate to be correct. The semaphore is its own state.

So which do you choose? Use a semaphore when the thing you are waiting on is naturally a count — free slots, available items, permits in a pool — because that count is exactly what a semaphore stores. Use a condition variable when the thing you are waiting on is a richer condition that no single integer captures — 'the queue is non-empty AND not being resized', or 'the buffer holds at least 64 bytes'. The honest summary: a semaphore is the right tool precisely when your predicate happens to be 'a counter is above zero', and a condition variable is the general tool for every predicate that is not.

The payoff: producer-consumer in a few lines

Now the reward. The classic producer-consumer problem — the subject of the very next guide — has one or more threads producing items into a fixed-size shared buffer and others consuming them out. Two waits are needed and they pull in opposite directions: a producer must wait when the buffer is full (no empty slot to put into), and a consumer must wait when the buffer is empty (no item to take). Each of those waits is a count: 'how many empty slots?' and 'how many filled slots?'. Two counts means two semaphores, and they map onto the problem almost magically.

  Bounded buffer of N slots.  Two counting semaphores:
     empty  = sem_init(N)   /* slots free to write : starts at N */
     filled = sem_init(0)   /* items ready to read : starts at 0 */
  Plus one mutex 'm' so only one thread edits the buffer at a time.

   PRODUCER loop:                  CONSUMER loop:
     item = make_item();             sem_wait(&filled);  /* need an item */
     sem_wait(&empty);  /* need a    sem_wait... */
                          free slot*/   pthread_mutex_lock(&m);
     pthread_mutex_lock(&m);           x = buffer_take();
     buffer_put(item);                 pthread_mutex_unlock(&m);
     pthread_mutex_unlock(&m);         sem_post(&empty);  /* freed a slot */
     sem_post(&filled); /* an item    use(x);
                          is ready */
The bounded-buffer solution. empty counts free slots, filled counts ready items; the mutex protects the buffer edit itself. Each side waits on one semaphore and posts the other.

Trace it once and the gears mesh. A producer calls sem_wait(&empty): if every slot is full, empty has hit 0 and the producer sleeps right there until a consumer frees a slot. A consumer calls sem_wait(&filled): if nothing is ready, filled is 0 and it sleeps until a producer posts. When a producer finishes putting, its sem_post(&filled) raises the filled count and wakes a sleeping consumer; when a consumer finishes taking, its sem_post(&empty) raises empty and wakes a sleeping producer. The counts breathe in and out between the two sides — no polling, no busy-waiting, no lost wakeups, because every post is banked in a count.

One detail rewards a careful eye, and it foreshadows the final guide. Notice the order on the producer side: sem_wait(&empty) comes before pthread_mutex_lock(&m), never after. Flip them — grab the mutex first, then block on a full buffer's empty semaphore while still holding it — and a producer would fall asleep clutching the very lock a consumer needs in order to take an item and free a slot. Nobody could ever make progress. That is a deadlock, and the rule it whispers — acquire your 'permission to proceed' semaphores before the short mutex that guards the data — is exactly the kind of ordering discipline the deadlock guide makes systematic.

Honest edges and what comes next

Semaphores are elegant, but be honest about their sharp edges, because they are easy to get subtly wrong. They are unstructured: nothing in the language pairs a sem_wait() with its matching sem_post() the way a scope pairs a lock with its unlock. Post one too few times and a thread sleeps forever; post one too many and you have handed out a permit that does not exist — the resource pool overflows its real capacity and two threads collide on one connection. There is no owner to check, no compiler to warn you. The count is only ever as correct as your own bookkeeping makes it.

Step back and hold the shape. A semaphore is one atomic, non-negative counter touched only by sem_wait() (take one, or sleep if none) and sem_post() (give one, wake a waiter). Use it as a counting semaphore to cap how many threads share a pool, or as a binary semaphore to signal an event across threads — and unlike a condition variable it remembers posts, so it needs no companion predicate of its own. You now hold the last primitive this rung introduces. The next guide spends a whole chapter inside the producer-consumer pattern we just sketched, and the one after that names the deadlock we just glimpsed and gives you the discipline to avoid it.