C++counting_semaphore通用计数信号
C++20 counting_semaphore:通用计数信号量的原理与实践
在多线程编程中,同步原语是保障数据一致性和执行时序的关键工具。C++20 标准正式引入了 <semaphore> 头文件,其中 std::counting_semaphore 作为首个标准化的无锁(lock-free)计数信号量,为开发者提供了比互斥量更轻量、比条件变量更直接的资源协调机制。它不依赖于任何特定线程的“所有权”,仅通过原子计数器管理许可(permits)的获取与释放,适用于生产者-消费者、限流控制、任务配额分配等多种并发场景。
counting_semaphore 的核心语义简洁而强大:它维护一个非负整型计数器(value),初始值由模板参数 LeastMaxValue 和构造函数参数共同约束;调用 acquire() 会阻塞直至计数器大于 0,随后原子地将其减 1;调用 release() 则原子地将计数器加 1(可指定增量,默认为 1)。值得注意的是,该类型要求底层计数器支持无锁操作,因此其 max() 值受平台原子操作能力限制(通常为 std::numeric_limits<difference_type>::max() 的合理子集)。
以下是一个典型的生产者-消费者模型示例,展示如何使用 counting_semaphore 控制缓冲区访问:
#include <semaphore>
#include <vector>
#include <thread>
#include <iostream>
#include <chrono>
#include <random>
// 固定大小的线程安全缓冲区(仅作示意,不包含完整边界检查)
struct BoundedBuffer {
static constexpr size_t CAPACITY = 4;
std::vector<int> data;
std::counting_semaphore<> slots{CAPACITY}; // 空闲槽位数,初始=容量
std::counting_semaphore<> items{0}; // 已填充项数,初始=0
BoundedBuffer() : data(CAPACITY) {}
void produce(int value) {
slots.acquire(); // 等待空槽
// 写入临界区(此处简化,实际需配合 mutex 或无锁结构)
size_t idx = (CAPACITY - slots.try_acquire_until(
std::chrono::steady_clock::now() + 10ms)) % CAPACITY;
data[idx] = value;
items.release(); // 通知有新项
}
int consume() {
items.acquire(); // 等待可用项
// 读取临界区(同上)
size_t idx = (items.try_acquire_until(
std::chrono::steady_clock::now() + 10ms) ? 0 : 0) % CAPACITY;
int val = data[idx];
slots.release(); // 归还空槽
return val;
}
};
上述代码虽为示意,但清晰体现了信号量的职责分离:slots 控制写入许可,items 控制读取许可。二者协同消除了忙等与死锁风险,且无需显式锁竞争。相比 std::mutex + std::condition_variable 组合,代码行数减少约 40%,逻辑路径更线性。
counting_semaphore 还支持超时等待,增强程序鲁棒性。try_acquire_for() 和 try_acquire_until() 在指定时间内未获得许可时返回 false,避免无限阻塞。例如,在网络请求限流器中可这样使用:
#include <semaphore>
#include <thread>
#include <chrono>
class RateLimiter {
std::counting_semaphore<> permits;
public:
explicit RateLimiter(size_t max_concurrent) : permits(max_concurrent) {}
bool try_acquire(std::chrono::milliseconds timeout) {
return permits.try_acquire_for(timeout);
}
void release() {
permits.release();
}
};
// 使用示例:模拟并发请求处理
void handle_request(RateLimiter& limiter, int id) {
if (!limiter.try_acquire(500ms)) {
std::cout << "Request " << id << " timed out\n";
return;
}
// 模拟耗时操作
std::this_thread::sleep_for(200ms);
std::cout << "Request " << id << " completed\n";
limiter.release();
}
此限流器无需额外状态管理,所有同步逻辑内聚于信号量实例。当 max_concurrent 设为 3 时,任意时刻最多 3 个请求并行执行,其余等待或超时退出,天然满足令牌桶语义。
需特别注意几个关键约束:首先,counting_semaphore 不可拷贝、不可移动,仅支持默认构造(若 LeastMaxValue > 0)和带参构造;其次,release(n) 中 n 必须非负,且累加后不得超出 max(),否则行为未定义;最后,其 try_acquire_* 系列函数不抛异常,适合对异常敏感的实时系统。
相较于 POSIX sem_t 或 Windows Semaphore,std::counting_semaphore 的优势在于标准化接口、零成本抽象(编译期确定内存序)、以及与 C++ 并发库(如 std::jthread, std::stop_token)的无缝集成。它不替代互斥量——不保护数据本身,只协调访问时机;也不替代 std::barrier——不用于多阶段同步,而专注资源配额。
在工程实践中,建议将 counting_semaphore 用于明确存在“许可池”语义的场景:如数据库连接池、GPU 显存页分配、api 调用频次控制等。避免将其用于需要复杂状态判断的同步(此时应选用 std::condition_variable 或更高级的同步原语如 std::latch / std::barrier)。
总之,std::counting_semaphore 是 C++20 并发演进的重要里程碑。它以极简 api 封装了经久验证的计数信号量模型,兼顾性能、安全与可维护性。掌握其原理与适用边界,有助于构建更清晰、更可靠的高并发系统。随着 C++23 对 std::binary_semaphore 的补充及更多同步设施的完善,现代 C++ 的并发表达能力正持续走向成熟与统一。

