Semaphore Overview (C++)

Semaphore is a very important synchronization mechanism in concurrent programming (multithreading/multiprocessing), used to control access to shared resources by multiple threads or processes, preventing issues such as race conditions, data inconsistency, or deadlocks.Semaphore Overview (C++)

1. What is a Semaphore?

✅ Definition:

A semaphore is a synchronization tool used to control access to shared resources by multiple threads/processes, essentially a counter that manages the available quantity of resources through two core operations:

  1. 1.

    P operation (also known as wait/acquire/down): Attempts to acquire a resource, decrementing the counter by 1. If the counter is 0, it blocks and waits.

  2. 2.

    V operation (also known as signal/release/up): Releases a resource, incrementing the counter by 1. If other threads are waiting, it wakes one of them.

📌 These two operations are atomic, meaning they cannot be interrupted by other threads during execution, ensuring the integrity of the operations.

2. Types of Semaphores

1. Binary Semaphore

  • The counter can take values of 0 or 1, equivalent to a mutex, but the semaphore is not necessarily thread-exclusive (i.e., the thread holding the semaphore does not have to be the one releasing it).

  • Used to control access to critical resources by only one thread at a time.

2. Counting Semaphore

  • The counter can take values of non-negative integers (e.g., 0, 1, 2, …, N), used to control multiple threads accessing N identical resources simultaneously.

  • For example: In a thread pool with 5 database connections, a maximum of 5 threads can use them simultaneously, which is a counting semaphore with an initial value of 5.

3. Working Principle of Semaphores (Illustrated Understanding)

Assuming the initial value of the semaphore is N (e.g., 3), indicating that 3 resources are available:

Operation

Behavior

Counter Change

P() / wait() / acquire()

Request resource:• If counter > 0, decrement counter by 1 and continue execution• If counter == 0, thread blocks and waits for resource release

counter–

V() / signal() / release()

Release resource:• Increment counter by 1• If other threads are waiting for the resource, wake one of them

counter++

4. Use Cases of Semaphores

Semaphores are widely used in multithreaded programming, with typical scenarios including:

✅ 1. Limiting the number of threads accessing a resource simultaneously

  • For example: database connection pools, thread pools, API call rate limiting, allowing only N threads to access a device at the same time.

✅ 2. Producer-Consumer Problem

  • Control access to a buffer, coordinating synchronization between producer and consumer threads.

✅ 3. Thread Synchronization

  • For example: Thread A must wait for Thread B to complete a task before continuing, which can be coordinated using a semaphore.

✅ 4. Implementing Mutual Exclusion (similar to Mutex, but more flexible)

  • A binary semaphore (initial value of 1) can achieve similar functionality to a mutex, but the semaphore does not require the thread releasing the lock to be the one that acquired it (this is different from mutex, which is important to note).

5. Semaphores in C++ (Native Support from C++20)

✅ Starting from C++20, the standard library provides a <span><semaphore></span> header file with a standard semaphore class:

#include <semaphore>

Main Classes:

  • <span>std::counting_semaphore<N></span>: Counting semaphore, maximum value of N

  • <span>std::binary_semaphore</span> (available from C++20, equivalent to counting semaphore with a maximum of 1)

6. C++20 Semaphore Usage Example

▶ Example 1: Using Semaphore to Limit the Number of Concurrent Threads (e.g., up to 3)

#include <iostream>#include <thread>#include <vector>#include <semaphore>#include <chrono>// Allow up to 3 threads to accessstd::counting_semaphore<3> sem(3);  // Initial value is 3
void task(int id) {    sem.acquire();  // P operation: request resource, decrement counter by 1    std::cout << "Thread " << id << " is running..." << std::endl;    std::this_thread::sleep_for(std::chrono::seconds(2));  // Simulate time-consuming operation    std::cout << "Thread " << id << " has finished running." << std::endl;    sem.release();  // V operation: release resource, increment counter by 1}
int main() {    std::vector<std::thread> threads;
    for (int i = 0; i < 10; ++i) {        threads.emplace_back(task, i);    }
    for (auto& t : threads) {        t.join();    }
    return 0;}

🔍 Note:

  • A counting semaphore is created, initially allowing 3 threads to run simultaneously.

  • Each thread calls <span>sem.acquire()</span> before running; if there are already 3 threads running, the new thread will block until a semaphore is released.

  • After running, it calls <span>sem.release()</span> to release the resource.

▶ Example 2: Using Semaphore to Implement Simple Mutual Exclusion (Similar to Mutex, Binary Semaphore)

#include <iostream>#include <thread>#include <semaphore>
std::binary_semaphore sem(1);  // Binary semaphore, initial value 1 (similar to mutex)
int shared_data = 0;
void increment() {    sem.acquire();    int temp = shared_data;    std::this_thread::sleep_for(std::chrono::milliseconds(100));    shared_data = temp + 1;    std::cout << "shared_data = " << shared_data << std::endl;    sem.release();}
int main() {    std::thread t1(increment);    std::thread t2(increment);
    t1.join();    t2.join();
    std::cout << "Final result: " << shared_data << std::endl;  // Should be 2, no race condition    return 0;}

🔒 Note:

  • Here, <span>std::binary_semaphore</span> (initial value 1) is used to protect access to <span>shared_data</span>, providing mutual exclusion similar to <span>std::mutex</span>.

7. If You Are Not Using C++20 (e.g., C++11/14/17)

In C++11 ~ C++17, the standard library does not provide semaphores directly, but you can:

Method 1: Use POSIX Semaphores (Linux/Unix)

  • Header file:<span><semaphore.h></span>

  • Functions:<span>sem_init</span>, <span>sem_wait</span>, <span>sem_post</span>, <span>sem_destroy</span>

Method 2: Use Windows Semaphores

  • API:<span>CreateSemaphore</span>, <span>WaitForSingleObject</span>, <span>ReleaseSemaphore</span>, <span>CloseHandle</span>

Method 3:Simulate Semaphores with Condition Variables + Mutex (Recommended Cross-Platform Method)

  • Manually implement semaphore P/V operations using <span>std::mutex</span> and <span>std::condition_variable</span>.

▶ Simple Semaphore Simulation Implementation (C++11, Using Mutex + Condition Variable)

#include <mutex>#include <condition_variable>
class Semaphore {private:    std::mutex mtx;    std::condition_variable cv;    int count;
public:    Semaphore(int initial = 1) : count(initial) {}
    void acquire() {        std::unique_lock<std::mutex> lock(mtx);        cv.wait(lock, [this]() { return count > 0; }); // Wait until count > 0        --count;    }
    void release() {        std::lock_guard<std::mutex> lock(mtx);        ++count;        cv.notify_one(); // Wake one waiting thread    }};

You can use this <span>Semaphore</span> class just like a standard semaphore.

8. Semaphores vs Mutexes

Feature

Semaphore

Mutex

Essence

Counter, controlling access of multiple threads to N resources

Lock, allowing only one thread to access the resource at a time

Count

Can be multiple (e.g., 3 resources)

Can only be 1 (mutual exclusion)

Who Releases

Any thread can release (not required to be the locking thread)

Must be released by the locking thread

Usage

Resource counting, rate limiting, thread synchronization

Mutual access to shared resources

C++20 Support

Yes (<span>std::counting_semaphore</span>)

Yes (<span>std::mutex</span>)

9. Summary: Key Points of Semaphores

Item

Description

Definition

A semaphore is a counter used to control access to shared resources by multiple threads/processes

Core Operations

P (wait/acquire): acquire resource, decrement counter by 1, may blockV (signal/release): release resource, increment counter by 1, may wake other threads

Types

Binary semaphore (mutex), counting semaphore (allows multiple resources)

Use Cases

Limit concurrency, thread synchronization, producer-consumer, resource pool management

C++20 Support

Native support:<span>std::counting_semaphore</span>, <span>std::binary_semaphore</span>

C++11~17

No native semaphore, can simulate with mutex + condition_variable, or use system APIs

Difference from Mutex

Semaphores are more general, allowing multiple threads to access, and do not require the releaser to be the locker

✅ Recommended Learning Path:

  1. 1.

    Understand Basic Concepts: What is a semaphore, P/V operations, counting and binary semaphores.

  2. 2.

    Hands-on Coding: Write a few examples using <span>std::counting_semaphore</span> in C++20 (e.g., limit thread count, simulate mutual exclusion).

  3. 3.

    Understand Application Scenarios: Such as thread pools, connection pools, producer-consumer.

  4. 4.

    If not using C++20, learn to simulate semaphores with mutex + condition_variable.

Leave a Comment