Welcome back! In our last lesson, we successfully tackled the producer-consumer problem using a combination of a mutex and two counting semaphores. This demonstrated how different synchronization tools can work together to solve complex coordination challenges. We saw that one tool was used for exclusive access (the mutex), while the others were used to track counts of available items and empty slots.
This lesson will formalize that distinction. We will dive into the two primary types of semaphores—binary and counting—to clearly compare their characteristics and behavior. You will then learn how to apply them to solve common resource allocation problems that frequently appear in system design. This will solidify your understanding of which tool to choose for a given concurrency scenario.
The Two Flavors of Semaphores
In the last lesson, we used semaphores as counters to manage available slots and items in a buffer. This concept of a counter is central to how semaphores work, and the range of that counter is what distinguishes the two types.
A semaphore is essentially a special integer variable that supports two atomic operations:
wait()(orP()): Decrements the counter. If the counter is zero, the thread blocks until it becomes positive.signal()(orV()): Increments the counter, potentially unblocking a waiting thread.
The Neso Academy channel provides excellent, clear, step-by-step explanations of both binary and counting semaphores, showing how the counter changes as threads perform wait and signal operations.
These two segments will walk you through the mechanics of both semaphore types.
First, watch the detailed explanation of binary semaphores from this timestamp. Pay close attention to how the value, which can only be 0 or 1, is used to enforce mutual exclusion. The initial value is 1 (free), and it becomes 0 (busy) when a process enters the critical section. Next, watch the walkthrough of counting semaphores from this timestamp. Notice how the initial value is set to the number of available resource instances (e.g., 2), and the counter decrements as resources are acquired. A process only blocks when the counter reaches 0.
As the videos demonstrate, the core difference lies in their purpose and the values their internal counters can hold.
-
Binary Semaphore: The counter is restricted to 0 and 1. It acts like a switch. It's primarily used for two purposes:
- Mutual Exclusion: To ensure only one thread can enter a critical section at a time, similar to a mutex.
- Signaling: To allow one thread to signal to another that an event has occurred (e.g., "initialization complete").
-
Counting Semaphore: The counter can range over any non-negative integer value (0 to N). It acts like a manager for a pool of resources. Its main purpose is to control access to a set of
Nidentical, interchangeable resources, such as:- Connections in a database connection pool.
- Worker threads in a thread pool.
- Slots in a bounded buffer (as we saw in the producer-consumer problem).
This table from an AlgoMaster.io article provides a concise summary.
Semaphores | Concurrency Interview - AlgoMaster.io
This article offers a practical breakdown of semaphore types and their use cases.
Please read the section titled Binary vs Counting Semaphores. The table is particularly useful for a quick side-by-side comparison, and the "When to Use Each" part solidifies the conceptual difference.
Mutex vs. Semaphore: The Crucial Difference of Ownership
At first glance, a binary semaphore used for mutual exclusion seems identical to a mutex. Both can be used to ensure only one thread accesses a resource. However, there is a fundamental difference that has significant design implications: ownership.

- A Mutex is "owned." The same thread that acquires (locks) the mutex is the only one that can release (unlock) it. If another thread tries to unlock it, the system will raise an error or exhibit undefined behavior.
- A Semaphore has no concept of ownership. Any thread can call
wait()(to acquire a permit) and any thread can callsignal()(to release a permit), regardless of which thread performed the initialwait().
This distinction is not just academic; it enables different programming patterns. The lack of ownership is precisely what allows semaphores to be used for signaling between different threads, as we saw in the producer-consumer problem where a consumer signal()'s the empty semaphore for a producer to wait() on.
The following resource explains this critical difference in more detail.
Semaphores | Concurrency Interview - AlgoMaster.io
This section of the AlgoMaster.io article expands on the ownership difference and its practical consequences.
Read the section titled Semaphore vs Mutex. Focus on the table that compares the two and the explanation under "Why Ownership Matters." Then, review the clear guidelines in the When to Use Each subsection.
To summarize the guidance:
- Use a mutex when you are protecting a critical section and the logic requires the same thread to lock and unlock. This is the most common case for protecting shared data structures.
- Use a semaphore when you are counting access to a pool of resources or when you need to signal between different threads (like in producer-consumer patterns).
Applying Semaphores to Resource Allocation
The most common use case for a counting semaphore is managing a finite pool of resources. Imagine your system has a license for 10 simultaneous database connections, or you want to limit the number of concurrent video encoding jobs to 8 to avoid overloading the CPU.
A counting semaphore provides a clean and robust way to enforce these limits.
The pattern is straightforward:
- Initialize a counting semaphore with a value equal to the number of available resources,
N. - Any thread that wants to use a resource must first call
wait()on the semaphore. This decrements the count. If the count is zero, the thread blocks until a resource is freed. - When the thread is finished with the resource, it calls
signal()on the semaphore. This increments the count and wakes up a blocked thread, if any.
This video gives a great, practical analogy of managing a set of printers in an office.
99% of Developers Don't Get Semaphores
This segment from "The Coding Gopher" uses a Go-based example to illustrate how a counting semaphore limits access to a pool of three printers for 10 employees.
Watch the segment from this timestamp. The analogy is very intuitive and clearly shows how the semaphore acts as a gatekeeper, preventing resource exhaustion. The mention of database connection pools and API rate limits connects this directly to common challenges in backend engineering.
A More Complex Allocation Problem: Readers-Writers
A more advanced resource allocation challenge is the Readers-Writers Problem. In this scenario, a shared resource can be accessed in two ways:
- Reading: Multiple threads (readers) can read the resource simultaneously without issue.
- Writing: Only one thread (a writer) can modify the resource at a time, and no readers can be active while a write is in progress.
This problem can be solved using a combination of semaphores. A common solution involves:
- A binary semaphore (let's call it
rw_mutex) that is shared by both readers and writers. It ensures a writer gets exclusive access. - A counter to track the number of active readers.
- A mutex to protect the counter itself, ensuring that updates to the reader count are atomic.
The logic is that the first reader to arrive locks rw_mutex, preventing any writers from starting. Subsequent readers can join without re-locking rw_mutex. The last reader to leave unlocks rw_mutex, allowing a waiting writer to proceed. Any writer must lock rw_mutex before writing, which blocks both other writers and any new readers.
Operating Systems: Process Synchronization
These lecture notes from the University of Illinois Chicago describe the readers-writers problem and its semaphore-based solution.
Read the section titled The Readers-Writers Problem. You don't need to memorize the code, but focus on understanding the roles of rw_mutex, the readcount variable, and the mutex that protects readcount. This demonstrates how multiple synchronization primitives can be composed to enforce more complex access rules.
Conclusion
In this lesson, we formalized the distinction between binary and counting semaphores and saw how they compare to mutexes. This knowledge is fundamental for making correct design decisions in concurrent systems.
Key Takeaways:
- Binary semaphores have a count of 0 or 1 and are ideal for simple mutual exclusion or signaling between two threads.
- Counting semaphores have a count from 0 to N and are designed to manage access to a pool of
Ninterchangeable resources. - The primary difference between a mutex and a binary semaphore is ownership. A mutex must be unlocked by the thread that locked it, whereas any thread can
signala semaphore. - Counting semaphores provide an elegant solution to resource allocation problems, such as managing database connection pools, thread pools, or enforcing API rate limits.
Now that we have explored several tools for managing concurrency, it is time to discuss the primary danger that arises from their use: deadlock. In our next lesson, we will identify the four conditions that can lead to deadlock and begin exploring strategies to prevent or resolve it.