72 / 80 · C++17 · 约 10 分钟
互斥与 RAII:mutex、scoped_lock 和死锁
互斥锁保护的是一组共享状态的不变量,而不是变量名字。用 RAII 绑定锁的生命周期;同时更新多个对象时,一次获取所需锁。避免锁顺序环路,也避免在持锁期间等待线程或调用未知代码。
锁保护整个操作,而非单次读写
多个线程访问同一份普通可变状态时,应明确哪一把锁负责它,并让所有冲突访问遵守相同协议。只给写操作加锁而让读取裸奔,仍可能发生数据竞争。一次解锁与随后取得同一互斥锁的加锁操作建立同步,使后来的临界区能观察到前面的修改。
以账户转账为例,扣款和入账共同维护总余额不变。分别锁住单次赋值并不足够:其他线程可能看到只扣未入的中间状态。需要在同一临界区内检查条件并修改两端;共享状态的约束决定锁范围,而不是机械地每行代码放一把锁。
按所需能力选择 RAII 锁
lock_guard 构造时加锁、析构时解锁,适合一段固定的临界区。unique_lock 可以移动、延迟加锁和暂时解锁,条件变量等待需要这种可转交锁状态的能力。两者都让提前返回和异常展开自动释放已取得的锁,避免手写 lock/unlock 配对遗漏。
C++17 的 scoped_lock 可以同时管理多把锁,内部使用死锁规避算法取得它们。示例把两个账户的锁一次交给它,另一个线程即使按相反参数顺序转账,也不会形成这种双锁获取的死锁。锁对象必须有名字,否则临时对象在语句末尾销毁,后续访问已经没有保护。
局部避锁不等于全程序无死锁
死锁常见结构是甲持有 A 等待 B,乙持有 B 等待 A。统一锁层级或者同时获取完整锁集合,可以消除这类环。不要把同一个非递归互斥锁重复传给 scoped_lock;示例先识别向自己转账,直接返回,避免同一对象被重复加锁。
scoped_lock 不会分析所有外部依赖。若持锁调用回调、等待 future 或 join,而被等待方又需要这把锁,仍可能死锁。应把耗时计算移出临界区,只保留检查和提交状态。示例每方只转账一百次,初始余额足够覆盖任何交错,最终结果与调度顺序无关;主线程在 join 后读取余额,此时已无并发写入。
容易答错的地方
- 给同一份余额的读写分别使用两把不同 mutex,不能建立互斥关系;保护协议必须对应同一把锁。
- recursive_mutex 只能允许同线程重复加锁,不能消除跨线程等待环,也不能代替清晰的接口边界。
运行一个例子
最低标准 C++17 · 完整程序 · 下载 .cpp
#include <cassert>
#include <iostream>
#include <mutex>
#include <thread>
struct Account {
std::mutex mutex;
int balance = 1000;
};
void transfer_one(Account& from, Account& to) {
if (&from == &to) return;
std::scoped_lock lock(from.mutex, to.mutex);
assert(from.balance > 0);
--from.balance;
++to.balance;
}
int main() {
Account a;
Account b;
std::thread worker([&] {
for (int i = 0; i < 100; ++i) transfer_one(a, b);
});
for (int i = 0; i < 100; ++i) transfer_one(b, a);
worker.join();
assert(a.balance == 1000 && b.balance == 1000);
transfer_one(a, a);
assert(a.balance + b.balance == 2000);
std::cout << a.balance << ' ' << b.balance << '\n';
}
在本地编译
g++ -std=c++17 -Wall -Wextra -Wpedantic -pthread concurrency-mutex.cpp -o example && ./example预期结果
1000 1000
CHECK YOUR UNDERSTANDING
合上答案,试着解释。
若运行中新增读取两个账户总余额的函数,可以分别锁住并读取每个账户,再相加吗?给出一致快照的写法。
查看参考答案
不可以保证一致快照:两次读取之间可能完成一次转账,读到的余额来自不同状态。对不同账户,应使用 std::scoped_lock lock(a.mutex, b.mutex),持有两锁时计算并返回 a.balance + b.balance。若接口允许两参数指向同一账户,还须先处理别名,不能重复传入同一 mutex。
继续查证
标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。