std::semaphore 是 c++20 引入的轻量级同步原语,用于限制同时访问某类资源的线程数量;构造时传负数会直接 terminate,且需编译器支持 c++20。

std::semaphore 是 C++20 引入的轻量级同步原语
它不是用来替代 std::mutex 的,而是控制「同时能有多少个线程访问某类资源」——比如数据库连接池最多 5 个活跃连接、文件句柄上限 10 个、API 调用并发数限制为 3。它的核心是计数器 + 原子等待,比手写条件变量 + 互斥锁更简洁安全。
常见错误现象:std::semaphore 构造时传错初始值(比如传负数会直接 std::terminate),或在不支持 C++20 的编译器下误用(GCC
- 必须用 C++20 或更高标准编译:
g++ -std=c++20,且确保<semaphore></semaphore>头文件可用 - 初始化值代表“初始可用许可数”,不能为负;
std::binary_semaphore是特例,仅允许 0 或 1 - 调用
acquire()会阻塞直到有许可;try_acquire()非阻塞,返回bool - 注意:它不跟踪谁 acquire 了,也不保证 FIFO,仅保证计数正确
怎么用 acquire() 和 release() 控制资源数量
典型场景:限制 4 个线程同时执行某段耗资源操作。关键在于把 acquire() 放在临界区入口前,release() 放在出口后(哪怕异常也要保证释放)。
示例:
立即学习“C++免费学习笔记(深入)”;
std::semaphore sem{4}; // 允许最多 4 个并发
void worker() {
sem.acquire(); // 阻塞直到拿到一个许可
try {
do_heavy_work(); // 实际工作
} catch (...) {
sem.release(); // 必须放这里,否则许可泄漏
throw;
}
sem.release(); // 正常路径释放
}
-
acquire()可能抛出std::system_error(如系统资源不足),但实践中极少发生 -
release()不会阻塞,也不抛异常,但多调用会导致许可数超初始值——这不算错误,只是逻辑失控,需自己约束调用次数 - 不要在析构函数里隐式调用
release(),std::semaphore没有自动管理语义
std::binary_semaphore 和普通 semaphore 的区别在哪
std::binary_semaphore 是 std::semaphore 的特化,内部只允许计数值为 0 或 1。它比通用版更轻——某些平台可映射到 futex 或 Windows Slim Reader/Writer Lock,性能略优。
使用场景:需要“二选一”同步(如生产者-消费者中通知单个消费者),或模拟自旋锁语义(配合 try_acquire())。
- 构造时只能传 0 或 1:
std::binary_semaphore sem{1};合法,{2}编译失败 - 不能用
acquire()等待多个许可,因为根本不存在“多个” - 若你只需要开关型同步,优先用
std::binary_semaphore;需要配额控制(如限流),必须用std::semaphore
跨线程传递 semaphore 对象容易踩什么坑
std::semaphore 不可拷贝,但可移动——这点和 std::mutex 一致。误用拷贝会导致编译错误,而移动后原对象进入有效但未指定状态(不能再调用其成员函数)。
- 别写
auto s2 = s1;(编译失败);要传参就用引用或指针:void f(std::semaphore& sem) - 移动后立刻使用原对象,比如
s1.release(),行为未定义 - 不要把它放进
std::shared_ptr试图“共享所有权”——语义错乱,许可数不会因引用计数变化 - 如果多个模块需共用同一信号量,用全局变量、静态局部变量或显式传递引用,别绕弯子
std::semaphore 本身很简单,难的是把它嵌进真实流程里不漏许可、不错配线程生命周期、不和已有锁机制冲突。尤其要注意构造参数、异常路径释放、以及移动后的空悬状态——这些地方一错,问题往往延迟暴露。








