Semaphores are synchronization primitives invented by Edsger Dijkstra in the early 1960s to solve concurrency problems in multiprogramming systems. They act as generalized locks, controlling access to shared resources through atomic operations (wait and signal).
In C++, semaphores can be implemented using POSIX APIs or modern C++ libraries, and two key patterns are mutual exclusion and signaling.
Semaphores
A semaphore is a synchronization primitive built on atomic operations but offering a higher-level abstraction.
It maintains a non-negative integer counter:
- Counting semaphore: represents available resources (e.g., buffer slots).
Counter = number of identical resources (e.g., buffer slots, printers).
- Binary semaphore: acts like a lock but with structured wait/signal semantics.
Think of a semaphore as a traffic light:
- Green → threads can proceed (resource available). Semaphore count > 0, meaning resources are available.
- Red → threads must wait (resource unavailable). Semaphore count = 0, meaning no resources left; threads must wait.
Two atomic operations control it:
wait()(P/down): Decrements the count if >0; otherwise, blocks the thread until resources are available.signal()(V/up): Increments the count, potentially waking a waiting thread.
The atomic nature of wait and signal ensures no race conditions when multiple threads check or update the semaphore simultaneously.
Counting Semaphores allow multiple threads up to a defined limit.
A Binary Semaphore on the other hand can implement a mutex.
A mutex (mutual exclusion object) is essentially a lock that ensures only one thread can access a critical section at a time.
It can be implemented using a binary semaphore:
- Semaphore initialized to 1 → resource available.
wait()decrements it to 0 → lock acquired.signal()increments it back to 1 → lock released.
https://www.gurusoftware.com/what-are-semaphores-and-how-do-they-work/ is a great reference.
Implementing Semaphores
Key Principle: For every logical constraint in a concurrent system, use a semaphore to enforce it.
This ensures correctness by design, because the semaphore directly models the resource availability.
Using Semaphores in C++
In POSIX systems, you can use <semaphore.h>:
#include <iostream>
#include <pthread.h>
#include <semaphore.h>
sem_t sem;
void* worker(void* arg) {
sem_wait(&sem); // wait (P)
std::cout << "Thread " << pthread_self() << " entered critical section\n";
sem_post(&sem); // signal (V)
return nullptr;
}
int main() {
pthread_t t1, t2;
sem_init(&sem, 0, 1); // binary semaphore (mutex-like)
pthread_create(&t1, nullptr, worker, nullptr);
pthread_create(&t2, nullptr, worker, nullptr);
pthread_join(t1, nullptr);
pthread_join(t2, nullptr);
sem_destroy(&sem);
return 0;
}Scenario 1: Circular Buffer (Producer-Consumer Problem)
This is done using two semaphores.
Semaphore for empty slots:
- Initialized to buffer size.
- Producer must wait if empty slots = 0 (red light).
Semaphore for full slots:
- Initialized to 0.
- Consumer must wait if full slots = 0 (red light).
Scenario 2: Multiple Identical Resources (e.g., Printers)
Suppose there are 3 printers in a lab. The semaphore is initialized to 3.
Each thread requesting a printer performs wait:
- If count > 0, it proceeds (green light).
- If count = 0, it blocks (red light).
When a printer is freed, signal increments the count, allowing another thread to proceed.
Scenario 3: Database Connection Pool
- A web server has a pool of 10 database connections.
- Semaphore initialized to 10.
- Each incoming request must acquire a connection (wait).
- If all 10 are in use, new requests block until one is released (signal).
- Why semaphore helps:
- Avoids busy waiting: requests sleep until a connection is available.
- Prevents overload: ensures no more than 10 concurrent connections.
Semaphores embody the principle of raising the level of abstraction. They make concurrent programming safer and more predictable.
Semaphores are essentially the OS’s way of turning synchronization from a manual, error-prone task into a structured, reliable mechanism.