4. 互斥锁基础:std::mutex的使用、lock与unlock、std::lock_guard与RAII
多线程编程里,最让人头疼的问题是什么?
我个人觉得,就是数据竞争。两个线程同时读写同一个变量,结果就像两个人同时抢一支笔写字——写出来的东西鬼知道是谁的。嗯,这时候就需要互斥锁登场了。
4.1 为什么需要互斥锁?
先看一个经典的反面教材。假设我们有一个银行账户,两个线程同时往里存钱:
int balance = 0;
void deposit() {
for (int i = 0; i < 100000; ++i) {
balance = balance + 1; // 这里有问题!
}
}
// 主线程创建两个线程分别执行 deposit()
// 你猜最终 balance 是多少?
按理说,每个线程加10万次,结果应该是20万。但我告诉你,实际跑出来可能只有12万、15万,每次还不一样。为什么会这样?
因为 balance = balance + 1 这行代码,在CPU眼里其实是三步操作:
- 从内存读取 balance 到寄存器
- 在寄存器里加1
- 把结果写回内存
两个线程同时执行这三步,就会发生「你读的时候我还没写回去」的情况。结果就是,明明加了两次,实际只加了一次。这就是数据竞争。
4.2 std::mutex 的基本用法
C++11 提供了 std::mutex,它就是一把锁。用法很简单:
#include <mutex>
std::mutex mtx;
int balance = 0;
void deposit() {
for (int i = 0; i < 100000; ++i) {
mtx.lock(); // 加锁
balance = balance + 1;
mtx.unlock(); // 解锁
}
}
这样改完之后,每次只有一个线程能执行 balance = balance + 1,另一个线程必须等着。结果就稳定在20万了。
4.3 lock/unlock 的坑
手动 lock 和 unlock 有几个明显的痛点:
- 忘记 unlock:函数中间 return 了,或者抛出异常了,unlock 就没执行到
- 代码复杂:每个分支都要写 unlock,代码变得又臭又长
- 死锁风险:多个锁互相等待时,手动管理很容易出错
我曾经在一个网络服务模块里,因为一个提前 return 忘记 unlock,导致整个服务线程池全部卡死。线上报警响了,我还在纳闷为什么所有请求都超时了……嗯,从那以后我再也不敢裸用 lock/unlock 了。
4.4 std::lock_guard:RAII 的救星
C++ 的 RAII(资源获取即初始化)思想,完美解决了这个问题。std::lock_guard 就是 RAII 在互斥锁上的应用:
void deposit() {
for (int i = 0; i < 100000; ++i) {
std::lock_guard<std::mutex> guard(mtx); // 构造时 lock
balance = balance + 1;
// 出作用域时自动 unlock
}
}
你看,代码简洁多了。guard 对象在构造时调用 mtx.lock(),在析构时调用 mtx.unlock()。不管函数是正常返回、中途 return、还是抛出异常,只要 guard 对象被销毁,锁就一定会释放。
4.5 lock_guard 的局限性
当然,lock_guard 也不是万能的。它有几个限制:
- 不能手动解锁:锁的持有时间完全由作用域决定,不能提前释放
- 不能转移所有权:lock_guard 不可拷贝,也不可移动
- 不能用于条件变量:条件变量需要更灵活的锁操作
如果你需要更灵活的控制,可以用 std::unique_lock。它支持手动 lock/unlock,也支持移动语义。不过那是后面章节的内容了,这里先不展开。
4.6 知识体系总览
下面这张图,帮你理清本章的核心逻辑:
4.7 实战建议与避坑指南
最后,分享几个我在实际项目中积累的经验:
| 场景 | 推荐做法 | 原因 |
|---|---|---|
| 保护一个简单变量 | std::lock_guard | 代码简洁,不易出错 |
| 需要提前解锁 | std::unique_lock | 支持手动 lock/unlock |
| 配合条件变量 | std::unique_lock | 条件变量要求 unique_lock |
| 性能极致优化 | 裸 lock/unlock | 减少构造析构开销(极少用) |
好了,关于互斥锁的基础就聊到这里。说白了,记住两件事:第一,共享数据一定要加锁;第二,用 lock_guard 代替裸 lock/unlock。做到这两点,你的多线程代码就能避免大部分数据竞争的问题。
公众号:蓝海资料掘金营,微信deep3321