Skip to main content

Semaphores: Counting, Binary, and Their Use Cases

What This Concept Is

A semaphore is an integer counter with two atomic operations:

  • wait(s) / P(s) / sem_wait -- decrement; if the new value would be negative, block until a post arrives.
  • post(s) / V(s) / sem_post -- increment; wake one waiter if any are blocked.

No user code ever observes the internal counter between those operations. Two distinct regimes of use:

  • Counting semaphore (initial value N). Represents a pool of N identical resources. Works as "up to N threads may proceed." Used for throttling, connection pools, and producer-consumer slot accounting.
  • Binary semaphore (initial value 0 or 1). Represents a signaling flag. With init = 0, used as a one-shot "event" where thread A waits and thread B posts. With init = 1, structurally works as a mutex, but is not a substitute for one (see misconception).

Semaphores are older than condition variables. They collapse the mutex and the condition variable into a single primitive, which is both their power and their danger.

Why It Matters Here

Semaphores dominate in two settings:

  • Resource pools: database connection limits, bounded thread pools, file-descriptor budgets.
  • Cross-process signaling: POSIX named semaphores (sem_open) let unrelated processes coordinate, which pthread_cond_t cannot.

Knowing both the mutex+condvar idiom and the semaphore idiom lets you read codebases that use either.

Concrete Example

Bounded buffer with two counting semaphores and one mutex:

sem_t empty;  // number of empty slots; init N
sem_t full; // number of full slots; init 0
pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;

void put(int x) {
sem_wait(&empty);
pthread_mutex_lock(&m);
buf[tail] = x; tail = (tail + 1) % N;
pthread_mutex_unlock(&m);
sem_post(&full);
}
int get(void) {
sem_wait(&full);
pthread_mutex_lock(&m);
int x = buf[head]; head = (head + 1) % N;
pthread_mutex_unlock(&m);
sem_post(&empty);
return x;
}

Compare to the condvar version: there are no while (predicate) loops here, because the semaphore itself counts. That is cleaner for simple resource accounting but less flexible when predicates are compound.

A connection pool in one semaphore:

sem_t pool; sem_init(&pool, 0, MAX_CONN);
// per request:
sem_wait(&pool);
conn_t *c = borrow_connection();
... use ...
return_connection(c);
sem_post(&pool);

Throttling falls out almost for free.

Common Confusion / Misconception

"A binary semaphore is a mutex." Structurally similar, operationally dangerous. A real mutex tracks ownership: only the locking thread can unlock, and the mutex supports recursion, priority inheritance, and error checking. A binary semaphore is ownerless -- any thread can post -- which means you can accidentally unlock a "mutex" that someone else locked, and you lose priority inheritance.

"Semaphores are always better than condvars because they need no predicate check." Only for simple counting conditions. Predicates like "queue is empty AND shutdown is not requested" are awkward with a single semaphore and natural with a mutex + condvar + while-loop.

"Signal first, wait later is fine." With a sem_t initialized to 0, a late waiter blocks until a post arrives -- but with a condition variable, a signal before any waiter is silently dropped. Semaphores remember. Condvars do not.

How To Use It

Pick the right primitive for the shape of the coordination:

  1. Counting resources (slots, tokens, permits) -> counting semaphore.
  2. Mutual exclusion on shared state -> mutex.
  3. Waiting for a compound predicate -> mutex + condition variable.
  4. One-shot signal that must not be missed even if the receiver is late -> semaphore (init = 0) or a future/promise.

Never try to do all four with a single type.

Check Yourself

  1. Why is a binary semaphore not a safe general substitute for a mutex?
  2. Why does the semaphore bounded buffer use a mutex inside the wait/post envelope?
  3. What does it mean that semaphores "remember" posts and condvars do not?

Mini Drill or Application

Build a small connection pool with MAX = 5 permits using sem_t in C. Spawn 20 threads that each sem_wait, sleep 50 ms, sem_post. Print the number of threads active at every change. Verify it never exceeds 5.

Read This Only If Stuck