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 returns true if successful, or false if the counter was zero. Timed overloads (try_acquire_for and try_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.