Understanding std::counting_semaphore in C++
This article explores the purpose, mechanics, and practical
applications of std::counting_semaphore, introduced in
C++20 to enhance the standard concurrency toolkit. Readers will learn
how this synchronization primitive regulates concurrent access to finite
resource pools, how it fundamentally differs from mutual exclusion locks
(std::mutex), and how its non-ownership design enables
efficient thread signaling and producer-consumer workflows.
What is std::counting_semaphore?
Defined in the <semaphore> header,
std::counting_semaphore is a lightweight synchronization
primitive that maintains an internal counter. The counter represents the
number of available units of a shared resource. Threads request access
by decrementing the counter and return access by incrementing it.
The template is defined as:
template<std::ptrdiff_t LeastMaxValue = /* implementation-defined */>
class counting_semaphore;The template parameter LeastMaxValue specifies the
minimum guaranteed upper bound for the internal counter, allowing the
implementation to choose the most efficient hardware representation.
Core Operations
A std::counting_semaphore provides three primary
operations:
acquire(): Decrements the internal counter by one. If the counter is zero, the calling thread blocks until another thread increments the counter.try_acquire(): Attempts to decrement the counter without blocking. It immediately returnstrueif successful, orfalseif the counter was zero. Timed overloads (try_acquire_forandtry_acquire_until) allow waiting for a specified duration.release(std::ptrdiff_t update = 1): Increments the internal counter by the specified value, unblocking waiting threads accordingly.
The Purpose in Modern C++ Concurrency
Before C++20, limiting concurrent access to a resource pool or
implementing signaling between threads required combining a
std::mutex with a std::condition_variable, or
relying on platform-specific APIs. std::counting_semaphore
provides a standardized, lower-overhead alternative for these specific
architectural patterns.
1. Managing Finite Resource Pools
The primary purpose of a counting semaphore is throttling access to a fixed number of shared resources, such as database connections, worker slots, or network sockets. If a system allows a maximum of five concurrent file downloads, a semaphore initialized with a count of 5 ensures that at most five threads execute the download logic simultaneously. Subsequent threads block until an active thread releases its slot.
2. Thread Signaling Without Ownership
Unlike a mutex, a semaphore has no concept of ownership. A mutex requires the same thread that locked it to unlock it. Violating this rule results in undefined behavior.
In contrast, any thread can call release() on a
semaphore, regardless of which thread called acquire().
This makes std::counting_semaphore ideal for signaling
events between threads, such as notifying worker threads that work is
ready or notifying a supervisor thread that a background task has
finished.
3. Efficient Producer-Consumer Implementation
Semaphores simplify bounded buffer implementations in producer-consumer scenarios. By using two semaphores—one tracking available buffer slots and another tracking filled slots—producers and consumers can synchronize efficiently without the overhead and lock contention associated with conditional variables.
Binary Semaphore Specialization
C++20 also provides std::binary_semaphore, an alias for
std::counting_semaphore<1>. When configured with a
maximum count of 1, the primitive can only hold values of 0 or 1. It
acts as an efficient binary flag primarily used for signaling task
completion or managing access to single resources where thread ownership
is not required.