Facebook Pixel

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.

cpp
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.

cpp
signal(S) {
    S++;              // Increment: release one resource
}
Why Atomicity Matters
Both operations are atomic, meaning they run to completion without any interruption. If wait() were not atomic, two processes could both check S > 0 at the same time, both see it as true, and both proceed, completely breaking mutual exclusion. The atomicity of these operations is what makes semaphores safe in concurrent environments.

Two Types of Semaphores

TypeValue RangeUse CaseExample
Binary Semaphore0 or 1Single-resource mutual exclusionOne printer shared by multiple processes. S=1 means free, S=0 means taken.
Counting Semaphore0 to NMulti-resource pool management5 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.

text
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 free

Implementation in C++

cpp
#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:

text
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

StrengthsPitfalls
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.
The Most Common Semaphore Bug
If a process enters a critical section using wait() but crashes or takes an error path that skips the signal() call, every other process waiting on that semaphore will be blocked forever. This is why production code often wraps semaphore usage in try/finally blocks or RAII patterns to guarantee the signal is always called, even when exceptions occur.

Semaphore Trace

Question 1 of 1

Test your understanding of how semaphore values change during execution.

A counting semaphore is initialized to 3. Three processes (P1, P2, P3) each call wait() and enter the critical section. P1 finishes and calls signal(). Then P4 calls wait(). What is the value of the semaphore after P4 enters?
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.