资源竞争需精准识别临界区、合理选锁机制并规避死锁。临界区是多线程交错执行且结果依赖顺序的最小代码段;std::mutex适用于复杂逻辑,std::atomic仅限单变量简单操作;多锁必须用std::scoped_lock统一获取以防死锁;lock_guard应限于纯内存操作,避免长持有。

资源竞争问题不是靠“加锁”就能自动消失的,而是必须明确临界区、控制访问路径、并防止锁本身成为新瓶颈。直接上 std::mutex 很容易漏掉嵌套调用、异常路径或多个锁的顺序冲突。
怎么识别真正的临界区?
临界区不是“所有共享变量操作”,而是“多个线程可能交错执行且结果依赖执行顺序的代码段”。比如两个线程同时对 shared_data++,即使只一行,背后是读-改-写三步,不加保护必然出错。
- 常见误判:把整个函数体当临界区,导致锁粒度过粗,性能下降明显
- 真正要保护的:仅包含共享状态读写的最小连续块,比如
queue.push()+notify_one()这种组合操作 - 注意隐式共享:传入的
std::string或容器若被多线程修改,也属于临界资源
std::mutex 和 std::atomic 该怎么选?
std::mutex 适合保护复杂逻辑或多个变量的原子性;std::atomic 只适用于单个变量的简单读写/修改,且不能替代锁来保证逻辑一致性。
- 用
std::atomic<int></int>替代int计数器:安全,零开销 - 但
if (counter > 0) { counter--; do_something(); }这类判断+动作组合,std::atomic无法保证整体原子性,必须用std::mutex -
std::atomic_flag是唯一无锁保证的原子类型,适合实现自旋锁,但别在长操作里用,会吃满 CPU
多个互斥量一起用时最容易踩什么坑?
死锁几乎全发生在这里——不是锁没加,而是加锁顺序不一致或未统一所有权管理。
- 绝对避免:线程 A 先锁
mtx1再锁mtx2,线程 B 反过来,这是经典循环等待 - 正确做法:用
std::scoped_lock(C++17)或std::lock+std::adopt_lock,一次性获取全部锁,内部自动处理死锁规避 - 不要手动
mtx1.lock(); mtx2.lock();—— 中间抛异常会导致mtx1永远不释放 - 如果必须分步加锁(比如超时场景),用
try_lock并在失败时主动释放已持锁,而不是硬等
为什么 lock_guard 有时反而让问题更隐蔽?
std::lock_guard 看似省心,但它只管“自动解锁”,不管“是否该加锁”或“加锁范围是否合理”。
- 典型陷阱:在函数开头就构造
std::lock_guard,但后续有耗时 IO 或可能抛异常的调用,导致锁持有时间远超必要 - 更危险的是:跨函数传递已加锁的
std::mutex引用,调用方以为“锁已由上层管了”,结果临界区实际裸奔 - 建议:把
std::lock_guard的作用域严格限制在纯内存操作范围内;涉及 IO、系统调用、回调等,应先释放锁再执行
最常被忽略的点是:资源竞争和死锁往往共生。你修复了一个计数器竞争,却可能因锁顺序混乱引入死锁;而一个看似安全的 std::scoped_lock,若包裹了慢速操作,又会变成性能瓶颈。没有银弹,只有根据数据流和调用链做具体裁剪。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











