A semaphore is a synchronization primitive that maintains a counter of available permits. A thread or task acquires a permit before entering a guarded section and releases it afterward; when the counter reaches zero, further callers wait until a permit is returned. In concurrency, semaphores are used to limit how many threads may use a resource at the same time and to signal events between threads, and they are available in virtually every programming language and operating system.
The concept was introduced by Edsger Dijkstra in the 1960s with two operations, P (wait) and V (signal). Modern APIs use names such as acquire and release, sem_wait and sem_post, or WaitAsync and Release, but the semantics are the same.
How a semaphore counts and blocks
A semaphore holds a non-negative integer initialized to the number of permits. The two operations must be atomic, meaning no other thread can observe the counter halfway through a change.
- Acquire (P, wait): if the counter is greater than zero, decrement it and continue; otherwise block until a permit becomes available.
- Release (V, signal): increment the counter and, if any threads are waiting, wake one of them.
Internally, the operating system or runtime pairs the counter with a wait queue, so blocked threads sleep instead of consuming CPU in a busy loop. Some implementations add a fast path using an atomic compare-and-swap so that uncontended acquires avoid a system call entirely.
The table below lists the common operations across implementations.
| Operation | Counter effect | Behavior when no permit is free |
|---|---|---|
| Acquire | Decrements by one | Blocks until a release |
| Try-acquire | Decrements if possible | Returns false immediately |
| Timed acquire | Decrements if possible within a time limit | Fails after the timeout |
| Release | Increments by one | Wakes one waiting thread |
Counting and binary semaphores
Semaphores come in two main forms that differ in their initial and maximum count. The choice depends on whether the goal is to limit concurrency or to signal a single event.
| Type | Counter range | Typical purpose |
|---|---|---|
| Counting semaphore | 0 to N | Limit concurrent access to N resources |
| Binary semaphore | 0 or 1 | Signal an event or guard a single resource |
| Named semaphore | 0 to N, shared by name | Synchronize separate processes |
| Async semaphore | 0 to N, non-blocking waits | Limit concurrent tasks in async code |
A counting semaphore initialized to 10 lets ten threads proceed and queues the eleventh. A binary semaphore initialized to zero works as a signal: one thread waits on it, and another thread releases it when data is ready. In asynchronous programming, types such as SemaphoreSlim in .NET or asyncio.Semaphore in Python suspend a coroutine instead of blocking an operating system thread.
Semaphore compared with mutex and other primitives
A semaphore is often confused with a mutex, but the two differ in ownership and count. A mutex has an owner: only the thread that locked it may unlock it. A semaphore has no owner, so any thread may release a permit, which makes it suitable for signaling but also easier to misuse.
| Primitive | Ownership | Concurrent holders | Best used for |
|---|---|---|---|
| Semaphore | None | Up to N | Throttling and signaling |
| Mutex | Owning thread | One | Mutual exclusion on shared data |
| Condition variable | Used with a mutex | Not applicable | Waiting for a state change |
| Read-write lock | Owning threads | Many readers or one writer | Read-heavy shared data |
| Atomic operation | None | Lock-free | Single counters and flags |
Because a mutex records its owner, many implementations provide priority inheritance and error checks for wrong-thread unlocks. A binary semaphore lacks these protections, so a mutex is the better choice for plain mutual exclusion and a semaphore for limits and cross-thread signals.
Practical uses of semaphores
In practice, semaphores solve resource throttling and coordination problems that a single lock cannot express. The decision table below pairs common situations with an appropriate setup.
| Situation | Configuration | Reason |
|---|---|---|
| Connection pool of 20 | Counting semaphore with 20 permits | Never exceeds pool capacity |
| Rate of outbound API calls | Async semaphore with 5 to 10 permits | Protects a remote service |
| Producer and consumer buffer | Two counting semaphores, empty and full | Classic bounded buffer |
| Thread startup handshake | Binary semaphore starting at 0 | One-time signal |
| Cross-process license limit | Named semaphore | Shared across processes |
The bounded buffer pattern shows how semaphores combine. A semaphore named empty starts at the buffer size and a semaphore named full starts at zero. Producers acquire empty, insert an item under a mutex and release full; consumers do the reverse. The counters therefore track free and occupied slots without busy waiting, and neither side can overrun or underrun the buffer.
Always release a permit in a finally block or with a scoped guard, so that an exception does not leak it. A semaphore that slowly loses permits is a classic production problem: throughput declines gradually until the application stalls completely, often days after deployment.
Pitfalls and failure modes
Semaphores are powerful but unforgiving of small mistakes, and the resulting bugs are often intermittent.
- Permit leaks: a code path that returns without releasing reduces capacity permanently.
- Over-release: releasing more often than acquiring silently raises the limit, unless a maximum count is enforced.
- Deadlock: threads that each hold one semaphore and wait for another can block forever; acquire in a fixed order.
- Starvation: without a fair queue, some threads may wait far longer than others.
- Blocking in async code: a synchronous wait inside an event loop can freeze the whole thread pool.
Diagnosing these problems requires visibility into the permit count. Exposing the number of available permits and the queue length as metrics, and logging timed-out acquires, usually reveals a leak or an undersized limit long before users notice a stall.
In plain words
Picture a small restaurant with a bowl of five table cards at the door. A guest takes a card to sit down and returns it when leaving. When the bowl is empty, new guests wait at the door.
A semaphore is that bowl of cards for a computer program. It controls how many tasks may use something at the same time. Tasks that arrive too early simply wait their turn, and nothing gets overcrowded.
A real-world example
A team builds a service that generates PDF reports by calling an external rendering API. The API provider allows at most 8 concurrent requests per customer and rejects anything beyond that with an error. During month-end, users request hundreds of reports within minutes.
The developers wrap each API call in an async semaphore with 8 permits and release the permit in a finally block. Report requests now queue inside the service instead of failing at the provider. The error rate drops to zero, total processing time stays close to the theoretical minimum for 8 parallel calls, and the queue length becomes a useful metric for capacity planning.