Binary Semaphores in Operating Systems
Semaphores come in two flavors: counting semaphores and binary semaphores. This lesson focuses on binary semaphores specifically, which are the simpler of the two and arguably the most commonly used when you just need to protect a single shared resource.
What a Binary Semaphore Actually Is
A binary semaphore can only ever hold one of two values: 0 or 1. That is it. When the value is 1, the resource is free and a process can go ahead and use it. When it is 0, the resource is occupied and anyone else trying to get in has to wait.
The two operations that make this work are wait and signal.
wait(S)
Wait checks the semaphore value. If it is greater than zero, the process proceeds and the value drops to 0. If it is already 0, the process blocks until the value becomes 1 again.
wait(S) {
while (S <= 0); // Block: resource is occupied
S--; // Claim the resource (S goes from 1 to 0)
}signal(S)
Signal is what a process calls when it is done. It bumps the value back up to 1, which lets the next waiting process through.
signal(S) {
S++; // Release the resource (S goes from 0 to 1)
}Walking Through How It Works
Say you have two processes, P1 and P2, both needing access to the same resource. The semaphore starts at 1.
Initial: S = 1 (resource is free)
Step 1: P1 calls wait(S) -> S = 0 -> P1 enters critical section
Step 2: P2 calls wait(S) -> S is 0 -> P2 BLOCKS (waits)
... P1 does its work ...
Step 3: P1 calls signal(S) -> S = 1 -> P2 gets unblocked
Step 4: P2 calls wait(S) -> S = 0 -> P2 enters critical section
... P2 does its work ...
Step 5: P2 calls signal(S) -> S = 1 -> Resource is free againIt is a clean, orderly handoff between processes. At no point are both P1 and P2 inside the critical section simultaneously.
Implementation in C++
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <chrono>
class BinarySemaphore {
private:
int value;
std::mutex mtx;
std::condition_variable cv;
public:
BinarySemaphore() : value(1) {} // Initialize to 1 (free)
void wait() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [this]() { return value == 1; });
value = 0; // Resource is now taken
std::cout << "Resource acquired. Semaphore value: " << value << std::endl;
}
void signal() {
std::unique_lock<std::mutex> lock(mtx);
value = 1; // Resource is now free
std::cout << "Resource released. Semaphore value: " << value << std::endl;
cv.notify_one();
}
int getValue() { return value; }
};
BinarySemaphore sem;
void process(int id) {
std::cout << "Process " << id << " trying to enter critical section." << std::endl;
sem.wait(); // Entry section
// --- Critical Section ---
std::cout << "Process " << id << " is in the critical section." << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(800));
std::cout << "Process " << id << " is leaving the critical section." << std::endl;
sem.signal(); // Exit section
}
int main() {
std::cout << "Initial semaphore value: " << sem.getValue() << std::endl;
std::thread p1(process, 1);
std::thread p2(process, 2);
std::thread p3(process, 3);
p1.join();
p2.join();
p3.join();
std::cout << "All processes completed." << std::endl;
std::cout << "Final semaphore value: " << sem.getValue() << std::endl;
return 0;
}Output:
Initial semaphore value: 1
Process 1 trying to enter critical section.
Resource acquired. Semaphore value: 0
Process 1 is in the critical section.
Process 2 trying to enter critical section.
Process 3 trying to enter critical section.
Process 1 is leaving the critical section.
Resource released. Semaphore value: 1
Resource acquired. Semaphore value: 0
Process 2 is in the critical section.
Process 2 is leaving the critical section.
Resource released. Semaphore value: 1
Resource acquired. Semaphore value: 0
Process 3 is in the critical section.
Process 3 is leaving the critical section.
Resource released. Semaphore value: 1
All processes completed.
Final semaphore value: 1Process 1 gets in first, Processes 2 and 3 wait. As soon as 1 signals, the next one gets through. They take turns cleanly with no overlap.
Binary vs Counting Semaphore
| Factor | Binary Semaphore | Counting Semaphore |
|---|---|---|
| Values it can hold | Only 0 or 1 | Any non-negative integer |
| Use case | Single resource access | Multiple instances of a resource |
| Complexity | Simpler | Slightly more complex |
| Mutual Exclusion | Yes, strictly one process at a time | Depends on the count value |
| Example | Single printer, single file | Thread pool, connection pool |
The binary semaphore is essentially the simplest possible lock. You do not need it to count anything beyond free or occupied. For situations where exactly one process should be in the critical section at a time and there is only one instance of the resource, it is usually the right tool for the job.
When you have multiple identical resources and need to track how many are still available, that is when you reach for a counting semaphore instead.
