c++20 中用 std::counting_semaphore 可精确控制函数并发数:初始化为最大并发数(如 sem(3)),每次调用前 acquire()、结束后 release(),推荐 raii 封装防异常泄漏;旧标准需用 mutex+condition_variable+计数器模拟,切勿误用 mutex 或 recursive_mutex。

用 std::semaphore 控制函数并发数(C++20)
如果你的项目已升级到 C++20 且编译器支持 std::semaphore(如 GCC 11+、Clang 13+、MSVC 2022 17.5+),这是最直接的方式:用信号量对函数入口做计数限流。
它本质是维护一个原子计数器,acquire() 尝试减一,若为负则阻塞;release() 加一并唤醒等待者。和互斥锁不同,它不绑定“拥有者”,适合跨线程计数场景。
- 初始化时传入最大并发数,比如
std::counting_semaphore sem(3)表示最多 3 个线程同时进入 - 每个函数调用前必须
sem.acquire(),结束后必须sem.release()—— 缺一不可,否则会永久卡死或计数错乱 - 不能在异常路径中遗漏
release(),推荐用 RAII 封装(见下节)
手动实现信号量或用 std::mutex + condition_variable(C++11/14/17)
老标准没有 std::semaphore,但可以用 std::mutex + std::condition_variable + 计数器模拟一个计数信号量。关键不是“锁住函数”,而是“控制进入函数的许可数量”。
常见错误是只用一个 std::mutex —— 那只能做到串行(并发数=1),达不到“限制为 N”的目标。
- 需维护一个
int available和一个std::condition_variable -
acquire():加锁 → 循环等待available > 0→available--→ 解锁 -
release():加锁 →available++→notify_one()→ 解锁 - 注意:
notify_one()比notify_all()更高效,除非你明确需要唤醒全部等待线程
RAII 封装避免 release 遗漏
手写 acquire()/release() 容易在异常或提前 return 时漏掉 release(),导致信号量永久卡死。必须用 RAII 自动管理。
定义一个守卫类,构造时 acquire,析构时 release:
struct SemaphoreGuard {
std::counting_semaphore& sem;
SemaphoreGuard(std::counting_semaphore& s) : sem(s) { sem.acquire(); }
~SemaphoreGuard() { sem.release(); }
SemaphoreGuard(const SemaphoreGuard&) = delete;
SemaphoreGuard& operator=(const SemaphoreGuard&) = delete;
};
然后在函数开头写 SemaphoreGuard guard(sem);,后续无论怎么 return 或 throw,都会自动 release。
没 C++20?那就把 sem 替换成你自定义的信号量类实例,接口保持一致即可。
别把 std::mutex 当 semaphore 用
这是新手最常踩的坑:看到“限制并发”,第一反应是加个全局 std::mutex,然后在函数开头 lock()、结尾 unlock()。结果是——所有调用彻底串行,不管你想限 2 个还是 10 个,实际并发数永远是 1。
根本原因:std::mutex 是二元的(locked/unlocked),它只表达“是否被占用”,不表达“还剩几个名额”。要支持 N 并发,必须引入显式计数逻辑。
另一个典型误用是试图用 std::recursive_mutex:它解决的是同一线程重复加锁问题,和并发数控制完全无关。
真正需要关注的是:你限制的是“同一函数的并发调用数”,而不是“某段临界区的访问频率”——这两者语义完全不同,选错同步原语就会南辕北辙。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











