Semaphores for Process Synchronization
When multiple processes are running at the same time and sharing resources, you need a reliable way to make sure they do not collide inside a critical section. Semaphores are one of the most widely used tools for this.
At its simplest, a semaphore is just an integer variable that controls how many processes can access a shared resource at any given moment. The concept was introduced by Edsger Dijkstra in 1965, and it remains foundational to operating system design to this day.
The Two Operations
Everything about semaphores revolves around two atomic operations: wait and signal.
wait(S)
The wait operation checks the semaphore's value. If it is greater than zero, the process is allowed to proceed and the value gets decremented by one. If the value is already zero, the process blocks and sits there waiting until something changes.
wait(S) {
while (S <= 0); // Block if no resources available
S--; // Decrement: claim one resource
}signal(S)
The signal operation is the opposite. When a process finishes using the shared resource, it calls signal, which increments the semaphore value by one. If any other processes were blocked waiting, this gives one of them the green light to proceed.
signal(S) {
S++; // Increment: release one resource
}Two Types of Semaphores
| Type | Value Range | Use Case | Example |
|---|---|---|---|
| Binary Semaphore | 0 or 1 | Single-resource mutual exclusion | One printer shared by multiple processes. S=1 means free, S=0 means taken. |
| Counting Semaphore | 0 to N | Multi-resource pool management | 5 database connections. Initialize S=5. Each connection taken decrements S. |
Binary Semaphores can only hold two values: 0 or 1. When the value is 1, the resource is free. When it is 0, it is taken and anyone else trying to get in will block.
Counting Semaphores can hold any non-negative integer value. The value represents how many instances of a resource are currently available. When it hits zero, no resources are left and any further requests block until someone releases.
How It Plays Out in Practice
Take two processes, P1 and P2, both wanting access to the same shared resource. The semaphore S starts at 1.
Initial: S = 1
P1 calls wait(S) -> S becomes 0 -> P1 enters critical section
P2 calls wait(S) -> S is 0 -> P2 BLOCKS (waits)
|
| P1 finishes, calls signal(S) -> S becomes 1
|
v
P2 gets unblocked -> wait(S) -> S becomes 0 -> P2 enters critical section
P2 finishes, calls signal(S) -> S becomes 1 -> Resource is freeImplementation in C++
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
class Semaphore {
private:
int count;
std::mutex mtx;
std::condition_variable cv;
public:
Semaphore(int initial_count = 1) : count(initial_count) {}
void wait() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [this]() { return count > 0; });
count--;
}
void signal() {
std::unique_lock<std::mutex> lock(mtx);
count++;
cv.notify_one();
}
};
Semaphore sem(1); // Binary semaphore initialized to 1
void process(int id) {
std::cout << "Process " << id << " waiting to enter critical section." << std::endl;
sem.wait(); // Entry section
// --- Critical Section ---
std::cout << "Process " << id << " entered critical section." << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(500));
std::cout << "Process " << id << " leaving critical section." << std::endl;
sem.signal(); // Exit section
}
int main() {
std::thread t1(process, 1);
std::thread t2(process, 2);
std::thread t3(process, 3);
t1.join();
t2.join();
t3.join();
return 0;
}Output:
Process 1 waiting to enter critical section.
Process 2 waiting to enter critical section.
Process 3 waiting to enter critical section.
Process 1 entered critical section.
Process 1 leaving critical section.
Process 2 entered critical section.
Process 2 leaving critical section.
Process 3 entered critical section.
Process 3 leaving critical section.Only one process enters the critical section at a time, which is exactly what mutual exclusion requires.
Strengths and Pitfalls
| Strengths | Pitfalls |
|---|---|
| Flexible enough to handle both single-resource and multi-resource scenarios. | Forgetting to call signal() after a critical section causes other processes to block forever. |
| Straightforward to use once you understand the wait/signal pattern. | Calling wait() twice without a corresponding signal() creates a deadlock. |
| Work across a wide range of synchronization problems (mutual exclusion, ordering, resource pools). | wait() and signal() calls can be scattered across different parts of a program, making bugs hard to track. |
| Can be used for process ordering (not just mutual exclusion). | For complex scenarios, higher-level constructs like monitors are often a safer choice. |
Semaphore Trace
Question 1 of 1Test your understanding of how semaphore values change during execution.
