C++11中需手写信号量:用std::mutex保护计数器,std::condition_variable实现阻塞等待;acquire()等待count>0后减1,release()加1并notify_one/notify_all;注意跨线程调用、避免忙等、不可用atomic自旋替代。

用 std::mutex 和 std::condition_variable 手写一个线程安全的信号量
标准 C++ 库(C++11 起)没有提供 std::semaphore,直到 C++20 才加入(且部分编译器/STL 实现仍不完整支持)。所以多数项目里,你得自己实现一个轻量、可靠、可复用的信号量。核心思路是:用互斥锁保护计数器,用条件变量阻塞等待非零值。
常见错误是只用 std::mutex 加锁后自增/自减,却忘了在 wait() 时检查计数器是否为 0 —— 这会导致忙等或死锁。正确做法必须配合 std::condition_variable::wait() 的谓词重载版本。
- 构造时传入初始计数值(通常 ≥ 0),负值无意义,应断言或抛异常
-
acquire()(或常叫wait())会阻塞直到计数 > 0,然后原子地减 1 -
release()(或signal())无条件加 1,并通知至少一个等待者 - 避免在持有锁期间调用用户代码(比如自定义回调),防止死锁或异常传播破坏状态
C++20 std::counting_semaphore 的使用和兼容性坑
如果你能确定运行环境支持 C++20 且 STL 实现完整(如 libstdc++ 13+、libc++ 16+、MSVC 2022 17.5+),直接用 std::counting_semaphore 最省心。但它不是“开箱即用”——很多 CI 环境或嵌入式工具链仍卡在 C++17。
典型报错是 error: 'counting_semaphore' is not a member of 'std',别急着重写,先确认编译器和标准库版本。Clang/GCC 需显式开启 -std=c++20,且头文件是 <semaphore></semaphore>(不是 <thread></thread> 或 <mutex></mutex>)。
立即学习“C++免费学习笔记(深入)”;
- 初始化语法是
std::counting_semaphore<long> sem{3};</long>,模板参数必须是带符号整型,不能是int(某些旧实现会静默失败) -
sem.acquire()可能抛std::system_error(如中断或资源耗尽),生产代码应捕获 - Windows 上 MinGW-w64 对
<semaphore></semaphore>的支持滞后,建议优先测__cpp_lib_semaphore宏
为什么不用 std::atomic_int 单独实现?
有人尝试只靠 std::atomic_int + 自旋(busy-wait)模拟信号量,比如循环 load() 判断是否 > 0 再 fetch_sub()。这在低争用、短等待场景下看似可行,但实际问题明显:
- 高并发下 CPU 占用飙到 100%,尤其等待时间稍长时完全不可接受
- 无法响应线程取消或超时(
try_acquire_for()类接口根本没法做) - ABA 问题虽不致命,但
fetch_sub成功不代表“这次 wait 该被满足”,逻辑易错 - POSIX
sem_wait()和 WindowsWaitForSingleObject()都是内核级阻塞,用户态自旋违背设计本意
一个最小可用的 C++11 兼容实现示例
下面这个类足够用于大多数同步场景,不含异常规格说明(C++11 起已废弃),也不依赖第三方库:
class semaphore {
std::mutex mtx_;
std::condition_variable cv_;
long count_;
public:
explicit semaphore(long initial = 0) : count_(initial) {
if (initial < 0) throw std::invalid_argument("semaphore initial count must be >= 0");
}
void acquire() {
std::unique_lock<std::mutex> lock(mtx_);
cv_.wait(lock, [this] { return count_ > 0; });
--count_;
}
void release() {
std::lock_guard<std::mutex> lock(mtx_);
++count_;
cv_.notify_one(); // notify_all() 更安全但有性能代价,按需选
}
};
注意 cv_.notify_one() 是保守选择;如果多个线程在等同一个信号量(比如生产者-消费者模型中多个消费者),用 notify_all() 更合理,否则可能饿死。但唤醒全部会引发惊群效应,高频释放时建议压测确认影响。
真正容易被忽略的是:信号量的语义不是“锁”,它不绑定持有者。也就是说,acquire() 和 release() 可以由不同线程调用 —— 这点和 std::mutex 根本不同,用错就导致计数器混乱。


















