Semaphores: A Powerful Synchronization Construct
April 23, 2026 · Luciano Muratore
Semaphores: A Powerful Synchronization Construct
A Semaphore is one of the most fundamental synchronization constructs in concurrent programming. Where a mutex allows exactly one thread to proceed at a time, a semaphore generalizes this idea: it allows up to N threads to operate simultaneously, blocking the rest until a slot becomes available.
Its implementation is surprisingly compact. Two methods are all it takes: Acquire() and Release().
The Implementation
class Semaphore {
public:
Semaphore(int max_available) : max_available(max_available), taken_(0) {}
void Acquire() {
std::unique_lock<mutex> lock(mx_);
while (taken_ == max_available) {
cond_.wait(lock);
}
++taken_;
}
void Release() {
std::lock_guard<mutex> lock(mx_);
--taken_;
cond_.notify_all();
}
private:
int max_available;
int taken_;
std::mutex mx_;
condition_variable cond_;
};
The state is minimal: max_available is the capacity—how many threads are allowed in at once—and taken_ is the current count of threads that have acquired the semaphore and not yet released it.
Release(): Lock and Notify
void Release() {
std::lock_guard<mutex> lock(mx_);
--taken_;
cond_.notify_all();
}
Release uses lock_guard because the work here is short and unconditional: decrement taken_, then broadcast to all waiting threads. There is no need to release the lock early or wait on a condition—lock_guard acquires on construction and releases on destruction, which is exactly what we need.
Once taken_ is decremented, notify_all wakes every waiting thread. They will each recheck the condition in Acquire and, if a slot is now available, proceed.
Acquire(): Lock, Check, and Possibly Sleep
void Acquire() {
std::unique_lock<mutex> lock(mx_);
while (taken_ == max_available) {
cond_.wait(lock);
}
++taken_;
}
Acquire uses unique_lock because it needs to release the lock while sleeping. lock_guard cannot do this—it holds the lock for its entire lifetime. unique_lock, on the other hand, can temporarily release the lock when passed to cond_.wait, allowing other threads to call Release while this thread sleeps.
The while loop is essential. When notify_all wakes multiple threads at once, all of them compete to reacquire the lock. Only one proceeds at a time, and each must recheck whether a slot is actually available—not just assume it is because it was woken up. This guards against spurious wakeups as well.
Once the condition is satisfied—taken_ < max_available—the thread increments taken_ and proceeds.
Why unique_lock vs lock_guard?
The choice between the two is not arbitrary. It follows directly from what each method needs to do:
Releaseonly needs to hold the lock briefly to modify state and signal.lock_guardis sufficient.Acquiremay need to sleep while holding no lock, then reacquire it upon waking. Onlyunique_locksupports this.
This distinction is the key design decision in the implementation.
Summary
- A Semaphore generalizes mutual exclusion: it allows up to
max_availablethreads to operate concurrently. Acquireblocks a thread when the semaphore is saturated, usingunique_lockso the lock can be released during sleep.Releasedecrements the count and wakes all waiting threads, usinglock_guardfor a simple, scoped hold.- The
whileloop inAcquireguards against spurious wakeups and the race between multiple threads woken bynotify_all. - Final Insight: The difference between
lock_guardandunique_lockis not just a style choice—it reflects whether a thread needs to sleep and reawaken inside a critical section, or simply lock, act, and leave.