Articles

What Is a Semaphore? Concurrency Control Explained

A semaphore is a counter that limits how many threads can access a resource at once. How they work, semaphores vs mutexes, and common pitfalls.

The Lycoris Team The Lycoris Team · · 4 min read
Equations and diagrams on a chalkboard

A semaphore is a synchronization primitive that controls access to a shared resource by maintaining a counter: threads acquire it before using the resource and release it afterward, and the counter blocks new acquirers once it hits zero. It’s one of the oldest tools in concurrent programming, dating back to Edsger Dijkstra’s work in the 1960s, and it still shows up everywhere from operating system kernels to connection pools.

The core idea: a counter with two operations

A semaphore wraps a single integer and exposes two atomic operations, traditionally named P (from the Dutch proberen, “to test”) and V (verhogen, “to increment”), though most modern APIs call them acquire and release.

  • Acquire decrements the counter. If the counter is already zero, the calling thread blocks until another thread releases.
  • Release increments the counter and wakes up a waiting thread, if any.

The counter starts at some initial value, and that value is the whole point of a semaphore: it caps how many threads can hold the resource simultaneously, not just whether one thread holds it.

semaphore = Semaphore(3)   // allow up to 3 concurrent holders

semaphore.acquire()
// ... use the shared resource ...
semaphore.release()

Both operations are guaranteed atomic by the underlying implementation — no two threads can decrement the counter past zero at the same time, which is what makes a semaphore safe to use as a coordination point across threads that don’t otherwise communicate.

Counting semaphores vs binary semaphores

A counting semaphore initializes with a value greater than one, letting N threads through at once. This is the pattern behind connection pools, worker limits, and rate-limited resource access — for example, allowing at most 10 concurrent database connections out of a pool.

A binary semaphore initializes with a value of exactly one, so only a single thread can hold it at a time. Functionally this looks like a lock, but it isn’t quite the same thing as a mutex — see the next section for why that distinction matters.

Semaphores vs mutexes

These two are frequently confused because a binary semaphore behaves like a mutex in the simple case, but they solve different problems and have different rules:

MutexSemaphore
PurposeMutual exclusion (one owner)Resource counting (N owners)
OwnershipOwned by the thread that locked itNo ownership — any thread can release
Who can releaseOnly the locking threadAny thread, including one that never acquired
Typical useProtecting a critical sectionLimiting concurrent access to a pool of N resources
Priority inheritanceOften supportedNot inherently supported

The ownerless nature of a semaphore is both its strength and its main foot-gun. Because any thread can call release, it’s easy to write a bug where a thread releases a semaphore it never acquired, silently corrupting the count and letting more threads through than intended. A mutex protects against this by construction — only the owner can unlock it — which is why mutexes are the right default for protecting a single critical section, and semaphores are the right tool when the goal is genuinely about capping concurrency to N.

What semaphores are used for in practice

  • Connection pools and resource limits. Cap the number of concurrent database connections, file handles, or outbound HTTP requests a process makes at once.
  • Producer-consumer coordination. A classic pattern pairs two counting semaphores — one tracking empty slots, one tracking filled slots — to coordinate a bounded buffer between producers and consumers. See the producer-consumer problem for the full walkthrough.
  • Throttling parallelism. Worker pools use a semaphore initialized to the desired concurrency level so that, say, only 4 threads process a queue at once even if 100 items are waiting.
  • Signaling between threads. A semaphore initialized to zero can act as a simple signal: one thread blocks on acquire until another thread calls release, which is a lightweight way to implement “wait until ready” without a full condition variable.

Deadlocks and the dining philosophers connection

Semaphores don’t eliminate deadlock risk — they just give you a different set of ways to get it wrong. If two threads each hold one semaphore and block trying to acquire the other, you get the same circular-wait deadlock you’d see with locks. The classic illustration of this is the dining philosophers problem, which is traditionally solved (or broken) using semaphores to represent shared forks.

The usual mitigations are the same ones that apply to locks in general: acquire resources in a consistent global order, avoid holding one semaphore while blocking on another when possible, and prefer higher-level abstractions (thread pools, structured concurrency, async task queues) over hand-rolled semaphore logic wherever the language provides them.

Semaphores and databases

Databases use analogous ideas at a different layer. MVCC avoids blocking readers against writers entirely by keeping multiple versions of a row, while optimistic vs. pessimistic locking covers the tradeoff between “acquire a lock upfront” (closer to how a semaphore behaves) and “check for conflicts at commit time.” Understanding semaphores at the thread level makes it easier to reason about why database engines pick one locking strategy over another, since the same tension between throughput and correctness shows up in both.

The takeaway

A semaphore is a counter with atomic increment and decrement operations, used to cap how many threads can access a resource concurrently. A counting semaphore allows N holders; a binary semaphore allows one, but unlike a mutex it has no concept of ownership, so any thread can release it. Use a semaphore when the problem is genuinely about limiting concurrency to N — connection pools, worker throttling, producer-consumer buffers — and reach for a mutex when the problem is protecting a single critical section from concurrent access.

The Lycoris Team The Lycoris Team · · 4 min read

Mutex vs Semaphore: What's the Actual Difference

A mutex lets exactly one thread hold a lock at a time; a semaphore allows a fixed number of concurrent holders. Here's what that difference means in practice.

#Computer Science #Concurrency #Programming
The Lycoris Team The Lycoris Team · · 5 min read

The Dining Philosophers Problem, Explained

The dining philosophers problem is a classic model of deadlock and starvation: five philosophers, five forks, and a resource-sharing rule that can lock up.

#Computer Science #Concurrency #Programming
The Lycoris Team The Lycoris Team · · 4 min read

The Producer-Consumer Problem, Explained

The producer-consumer problem is a classic concurrency pattern: coordinating producers and consumers around a shared, bounded buffer safely.

#Computer Science #Concurrency #Programming