Semaphore
A semaphore is a synchronization primitive invented by Edsger Dijkstra in 1965, generalizing the mutex from a binary lock to a counting mechanism. Where a mutex permits only one thread to enter a critical section, a semaphore maintains a non-negative integer counter and permits up to that many threads to proceed simultaneously. When a thread calls wait (or P), the counter is decremented; if it would go negative, the thread blocks. When a thread calls signal (or V), the counter is incremented and a waiting thread is awakened. The semaphore is the mathematical minimalism of concurrency: two operations, one counter, infinite expressive power.
The semaphore's counting property makes it strictly more expressive than a mutex. A mutex is a semaphore initialized to one. A barrier — a synchronization point where all threads must arrive before any proceed — is a semaphore initialized to zero, used in conjunction with a counter. A resource pool — limiting the number of threads that can access a finite set of resources — is a semaphore initialized to the pool size. The same primitive, with different initial values and usage patterns, solves problems that would require entirely different mechanisms in less elegant designs.
Semaphore Types and Their Semantics
Dijkstra originally defined two semaphore variants: the binary semaphore (values 0 and 1, functionally equivalent to a mutex) and the counting semaphore (values 0 to N, controlling access to N identical resources). Modern systems have added variants: the named semaphore (shared across processes via the operating system), the unnamed semaphore (shared only among threads of a single process), and the POSIX semaphore (a standardized cross-platform interface).
The semaphore's semantics are deceptively simple and notoriously subtle. The wait and signal operations must be atomic — indivisible by the hardware — or the semaphore's own counter becomes a race condition. But even with atomicity, the semaphore introduces the dining philosophers problem: a canonical example of deadlock where five philosophers sit at a round table, each needing two forks (semaphores) to eat, and each acquiring one fork and waiting forever for the other. The problem is not a bug in the semaphore but a structural property of systems that enforce mutual exclusion without a global ordering of resources.
The producer-consumer problem reveals another semaphore pattern: one semaphore tracks empty slots in a buffer, another tracks full slots, and a mutex protects the buffer itself. The producer waits on empty, signals full; the consumer waits on full, signals empty. This pattern is so common that it has become a cliché of concurrency education — yet it remains the foundation of every operating system buffer, every message queue, every streaming pipeline.
Semaphores and Higher-Level Abstractions
Despite their elegance, semaphores are rarely used directly in modern application code. They are too low-level: a single missing signal or duplicated wait can deadlock the entire system, and these errors are nearly impossible to detect through static analysis. Higher-level abstractions — mutexes, monitors, condition variables, and atomic operations — have largely replaced semaphores in application programming. The semaphore survives primarily in operating system kernels, embedded systems, and formal specifications, where its minimalism is a virtue rather than a hazard.
This displacement reveals a pattern in the evolution of programming abstractions: primitives that are theoretically complete are practically inadequate because they place too much burden on the programmer. The semaphore is complete — it can express any synchronization pattern — but it is not safe. Every use is a proof obligation. Modern concurrency tools trade completeness for safety: they restrict what can be expressed in order to prevent what should not be expressed. The semaphore is the undifferentiated clay; the mutex and the monitor are the molded vessels.
The semaphore is the most beautiful and most dangerous idea in concurrent programming. It distills synchronization to a single integer, and in doing so, it places the entire burden of correctness on the programmer's discipline. Dijkstra gave us a mechanism of perfect generality; we responded by wrapping it in safer, more restricted abstractions. This is not a failure of the semaphore. It is a recognition that generality without guardrails is a form of violence against the programmer. The semaphore belongs in the kernel, in the specification, in the textbook — but not in the application code, where human fallibility must be assumed.
See also: Thread, Process, Concurrency, Mutex, Operating System, Dining Philosophers Problem, Producer-Consumer Problem, Monitor, Barrier Synchronization, Deadlock