The counting semaphore is a fundamental synchronization primitive in computer science, first introduced by Dutch computer scientist Edsger W. Dijkstra in 1965. Unlike a mutex (binary semaphore) which allows only one thread to access a resource at a time, a counting semaphore allows up to a specified number of threads to access shared resources concurrently. It maintains an internal counter that gets decremented when a thread acquires the semaphore and incremented when a thread releases it. If the counter would become negative upon acquisition, the requesting thread is blocked until another thread releases the semaphore, incrementing the counter back to a non-negative value.
Counting semaphores are versatile synchronization tools that solve several critical concurrency challenges. They excel at managing access to a pool of identical resources, such as connection pools, thread pools, or buffer pools. For example, a database connection pool might use a counting semaphore to limit the maximum number of simultaneous connections. They're also essential in producer-consumer scenarios where you need to limit the number of items in a queue or buffer. Counting semaphores can implement throttling mechanisms to limit the rate of operations, particularly useful in networking applications or I/O operations where system resources are finite. They also provide an elegant solution for implementing barriers and rendezvous points in parallel programming, where a specific number of threads must reach a certain point before any can proceed.
In C++20, std::counting_semaphore was introduced into the standard library, providing a portable and efficient implementation
of this pattern. This implementation includes both non-blocking (try_acquire) and blocking (acquire) methods, making it
versatile for different concurrency needs.
Basic usage pattern:
// Create a semaphore allowing up to 5 concurrent accesses
std::counting_semaphore<5> sem(5);
// Acquire the semaphore (blocks if count would go below 0)
sem.acquire();
// Try to acquire without blocking (returns false if would block)
if (sem.try_acquire()) {
// Resource acquired
} else {
// Could not acquire resource
}
// Release the semaphore
sem.release();- Allows precise control over concurrent access to resources
- More flexible than mutexes for managing pools of resources
- Provides both blocking and non-blocking acquisition options
- Thread-safe and efficient implementation in the standard library
- Can be used to implement various concurrent design patterns
- Misuse can lead to deadlocks if resources are not properly released
- Incorrect initial count can lead to unexpected behavior
- Does not inherently provide recursive acquisition like some mutex implementations
- The template parameter specifies the maximum value, which must be known at compile time
- Always match acquire calls with release calls
- Consider using RAII wrappers to ensure resources are released
- Be careful with exception safety to prevent resource leaks
- Use non-blocking acquisition when appropriate to prevent deadlocks
- Document the purpose and ownership rules of your semaphores
- "The Little Book of Semaphores" by Allen B. Downey
- "C++ Concurrency in Action" by Anthony Williams
- "Operating Systems: Three Easy Pieces" by Remzi H. Arpaci-Dusseau and Andrea C. Arpaci-Dusseau
- "Concurrency: The Works of Leslie Lamport"
- "Modern C++ Programming with Test-Driven Development" by Jeff Langr
This code is provided under the MIT License. Feel free to use, modify, and distribute as needed.
Contributions are welcome! Please feel free to submit a Pull Request.