因为 std::mutex 是二值的、不可重入、不支持计数,无法模拟带计数的信号量;需用 std::atomic + std::condition_variable 实现原子计数与条件等待。

为什么不能直接用 std::mutex 模拟信号量
因为 std::mutex 是二值的、不可重入、不支持计数——它只能“锁一次,解一次”,而信号量本质是带计数的资源池。比如你希望允许多个线程同时访问最多 3 个数据库连接,std::mutex 做不到,必须靠原子计数 + 条件等待。
用 std::atomic + std::condition_variable 实现核心逻辑
标准库没提供 std::semaphore(C++20 才有,且部分编译器支持不全),所以手动实现最稳妥。关键点是:计数器必须原子操作,等待逻辑必须配合互斥锁和条件变量,避免虚假唤醒。
-
count_用std::atomic<int></int>存储,初始化为初始值(如 3) -
wait()先原子减一,若结果 std::unique_lock +cv_.wait()阻塞;注意要循环检查,因为可能被虚假唤醒 -
signal()原子加一,若加之前值 cv_.notify_one() - 别忘了
cv_.notify_one()在锁外调用更安全,但标准做法是在锁内调用,避免丢失通知
class Semaphore {
std::atomic<int> count_;
std::mutex mtx_;
std::condition_variable cv_;
<p>public:
explicit Semaphore(int initial = 0) : count_(initial) {}</p><pre class="brush:php;toolbar:false;">void wait() {
int expected;
while (true) {
expected = count_.load();
if (expected > 0 && count_.compare_exchange_weak(expected, expected - 1))
return;
std::unique_lock<std::mutex> lock(mtx_);
cv_.wait(lock, [this] { return count_.load() > 0; });
}
}
void signal() {
int prev = count_.fetch_add(1);
if (prev < 0) cv_.notify_one();
}};
使用时容易踩的坑:初始化值、调用顺序、析构时机
信号量不是“用了就安全”,错误初始化或提前销毁会导致未定义行为。
立即学习“C++免费学习笔记(深入)”;
- 初始化值必须 ≥ 0;传负数会立刻卡死所有
wait()调用(因为没人能signal()) -
signal()可以在wait()前调用(即“预释放”),这是合法且常见的,比如初始化资源池 - 如果
Semaphore是局部对象,确保所有等待线程已退出再析构;否则cv_.notify_one()可能触发已销毁对象上的调用 - 不要在信号量上调用
wait()后忘记配对signal(),否则计数持续减少,最终阻塞
C++20 std::counting_semaphore 的兼容性现状
虽然标准有了,但实际落地要看编译器和 STL 版本:libstdc++(GCC)直到 13.2 才完整支持;libc++(Clang)需搭配较新版本;MSVC 2022 v17.5+ 支持,但默认 STL 可能仍禁用。启用前务必检查 __cpp_lib_counting_semaphore 宏。
即使可用,也建议先封装一层薄适配器,保留 fallback 到手写实现的路径——尤其在跨平台构建或嵌入式环境里,控制权比“标准”更重要。
真正麻烦的从来不是写几行原子操作,而是确认所有线程退出后才销毁,以及谁负责 signal() ——这往往取决于业务逻辑,而不是类本身能兜住的。









