很多并发 bug 不是因为语法写错,而是同步原语选错了。`std::mutex` 和 `std::counting_semaphore` 看起来都能“拦住线程”,但它们表达的约束完全不同:一个管临界区所有权,一个管可用资源数量。下面按语义、适用场景和常见误用拆开讲,方便你判断该用哪一个、哪些场景坚决不要替换着用。
`std::mutex`:它解决的是临界区保护
`std::mutex` 的设计目标很明确:保护共享数据所在的临界区,保证同一时刻只有一个线程进入。它表达的是排他访问,而不是“有人处理完了通知一下”。
这类互斥量带有线程所有权语义。也就是说,只有执行了 lock() 的那个线程,才能执行对应的 unlock()。如果换成别的线程去解锁,行为就是未定义的。标准库里的 std::mutex 通常不会主动替你检查,但在一些基于 pthread_mutex_t 的调试环境里,可能会报出 EPERM 之类的错误。
- 不支持跨线程释放,这不是缺点,而是边界保护
- 状态只有“已锁定 / 未锁定”,不负责资源数量计数
- 更推荐用
std::lock_guard或std::unique_lock做 RAII 封装,减少手动unlock()遗漏 - 如果同一线程存在递归进入同一临界区的需求,应考虑
std::recursive_mutex,而不是硬把std::mutex用成可重入锁
实际判断很简单:只要你的目标是“保护某个共享变量、容器或对象在修改时不被并发访问”,首选就应该是互斥量,而不是信号量。
`std::counting_semaphore`:它解决的是资源池计数
std::counting_semaphore 是 C++20 标准提供的计数型信号量,适合描述“当前还剩几个名额”“最多允许多少并发访问”这类问题。它的重点不是所有权,而是数量。
可以把它理解成“一个原子整数加一组等待线程”。当线程调用 acquire() 时,计数减 1;当计数为 0 时,线程阻塞等待。调用 release() 时,计数加 1,并唤醒等待者。这里最关键的一点是:调用 release() 的线程,不要求必须是此前执行过 acquire() 的线程。
- 它没有“当前持有者”这个概念,因此天然不适合表达互斥访问
- 初始化值可以是任意非负整数,例如
std::counting_semaphore<5>可用于表示最多 5 个并发访问名额 - 这种无所有权约束的设计很适合生产者-消费者、任务投递、资源池限流等模型
- 在 C++20 之前,标准库没有正式信号量,常见做法是用
std::condition_variable配合std::mutex模拟,但实现细节多,容易写错,性能也未必理想
也正因为如此,不要把 std::counting_semaphore<1> 当成 std::mutex 的替代品。即便它看起来也能“只放一个线程进去”,语义上仍然不是一回事:它不记录谁拿到了访问权,也不保证谁释放谁负责,代码可读性和安全边界都差一截。
为什么二者不能互相替代
很多误解来自表面行为相似:互斥量能阻塞线程,信号量也能阻塞线程,于是有人觉得只是“不同写法”。问题在于,并发原语最重要的不是能不能阻塞,而是它在代码里表达了什么约束。

互斥量强调的是排他性控制,核心问题是“现在能不能进”。信号量强调的是容量型协调,核心问题是“现在还能进几个”。前者适合给共享状态建立访问边界,后者适合给资源池、任务槽位或并发额度做数量限制。
如果把两者混用,维护成本会明显上升。读代码的人很难从同步原语本身判断你的意图:你到底是在保护共享内存,还是只是在做并发配额控制?语义一旦模糊,后续排查并发问题时就会特别痛苦。
常见误用:把信号量当互斥量,结果引出数据竞争
一个很典型的错误模式是:线程 A 先 acquire(),然后开始修改共享变量;线程 B 在别处调用 release(),把它当成“处理完成通知”;这时线程 C 可能提前 acquire() 成功,并在 A 还没写完时进入临界区,最终读到中间态数据。

问题的根源不在于信号量失效,而在于它从来就没有承诺“只有完成修改的线程才能释放访问权”。这是互斥量的职责,不是信号量的职责。
- 互斥量的
lock()/unlock()通常应该成对出现在同一作用域,或至少维持清晰的调用边界 - RAII 能自动保证异常路径和提前返回路径上的解锁行为,这也是它比手动调用更稳妥的原因
- 信号量的
acquire()/release()可以跨函数、跨线程、跨模块,灵活性很高,但一致性约束需要你自己保证 - 如果调试时发现
shared_data的值偶尔异常,先检查所有修改点是否都由同一把std::mutex保护,而不是只靠信号量在做“计数协调”
实际选型可以按这条线判断
当问题是“这段共享数据一次只能有一个线程改”,用 std::mutex。当问题是“这类资源最多允许 N 个线程同时占用”,用 std::counting_semaphore。
换句话说,互斥量处理的是访问排他,信号量处理的是容量协调。把红绿灯当停车位计数器,或者拿停车位计数器去决定谁能穿过路口,表面上都像是在“控流”,但规则完全不同。在 C++ 并发代码里,这种语义混淆往往就是 bug 的起点。









