C++ std::scoped_lock同时锁定多个互斥量避免死锁
在多线程编程中,当需要同时操作多个共享资源时,如果锁的顺序不一致,极易发生死锁。死锁会导致程序挂起,无法继续执行。C++17 引入了 std::scoped_lock,这是一个专门为了解决多锁死锁问题而设计的工具。它不仅能一次性锁定多个互斥量,还利用 RAII(资源获取即初始化)机制确保异常安全。
以下指南将详细介绍死锁的产生原因,以及如何通过 std::scoped_lock 彻底消除这一隐患。
1. 理解死锁的产生场景
在使用两个或多个互斥量(std::mutex)保护不同资源时,如果不同的线程以不同的顺序申请这些锁,就会形成循环等待。
假设有两个互斥量 mutex_a 和 mutex_b,以及两个线程:
- 线程 1:先锁定
mutex_a,再锁定mutex_b。 - 线程 2:先锁定
mutex_b,再锁定mutex_a。
当线程 1 持有 mutex_a 等待 mutex_b,而线程 2 持有 mutex_b 等待 mutex_a 时,两者永远无法继续。
为了更直观地理解这一过程,以下是死锁发生时的线程交互时序图:
为了避免这种情况,传统的做法是要求所有开发者必须严格遵守“全局固定的加锁顺序”。但在大型项目中,依靠人为规范来约束极其脆弱。std::scoped_lock 提供了编译器和库层面的解决方案。
2. 使用 std::scoped_lock 的核心步骤
std::scoped_lock 是一个可变参数模板,它接受多个互斥量作为参数。它内部使用了一种死锁避免算法(类似于 std::lock),能够以原子性的方式尝试锁定所有互斥量,要么全部锁定成功,要么全部不锁。
2.1 准备头文件和互斥量
在使用前,包含 <mutex> 头文件,并定义你需要的互斥量对象。
#include <iostream>
#include <thread>
#include <mutex>
#include <string>
std::mutex mutex1;
std::mutex mutex2;
2.2 编写并发任务函数
创建一个函数,在该函数中需要同时访问 mutex1 和 mutex2 保护的数据。
- 声明
std::scoped_lock对象,并将所有需要锁定的互斥量作为参数传递给构造函数。 - 构造函数会立即执行锁定操作。
- 编写临界区代码(即操作共享资源的代码)。
void critical_task(int id) {
// 第一步:同时锁定 mutex1 和 mutex2
// 这里使用了 C++17 的初始化语句语法
std::scoped_lock lock(mutex1, mutex2);
// 第二步:执行临界区操作
// 此时两个互斥量都已安全锁定,其他线程无法进入
std::cout << "Thread " << id << " is running critical section.\n";
// 模拟耗时操作
std::this_thread::sleep_for(std::chrono::milliseconds(100));
// 第三步:作用域结束,lock 对象析构
// 析构函数会自动释放 mutex1 和 mutex2
}
3. 实战案例:银行转账系统
为了展示 std::scoped_lock 的实际效用,我们模拟一个简单的转账场景。账户之间转账需要同时锁定转出账户和转入账户,防止数据竞争。
3.1 定义账户类
定义一个简单的 Account 类,包含余额和一个互斥量。
class Account {
public:
Account(std::string name, double money) : name_(name), money_(money) {}
double get_money() const {
return money_;
}
// 禁止拷贝构造和赋值,互斥量不可拷贝
Account(const Account&) = delete;
Account& operator=(const Account&) = delete;
// 声明友元函数,以便访问私有成员
friend void transfer(Account& from, Account& to, double amount);
private:
std::string name_;
double money_;
std::mutex mtx_; // 每个账户有自己的锁
};
3.2 实现无死锁转账函数
编写 transfer 函数。如果不使用 std::scoped_lock,当 A 向 B 转账的同时 B 向 A 转账,就会发生死锁。
void transfer(Account& from, Account& to, double amount) {
// 使用 std::scoped_lock 同时锁定两个账户的互斥量
// 注意:这里不需要手动调用 lock()
std::scoped_lock lock(from.mtx_, to.mtx_);
// 临界区开始
if (from.money_ >= amount) {
from.money_ -= amount;
to.money_ += amount;
std::cout << "Transferred " << amount << " from " << from.name_
<< " to " << to.name_ << "\n";
} else {
std::cout << "Transaction failed: " << from.name_ << " has insufficient funds.\n";
}
// 临界区结束,lock 离开作用域自动释放锁
}
3.3 运行并发测试
创建主函数,启动多个线程进行交叉转账。
int main() {
Account alice("Alice", 1000);
Account bob("Bob", 1000);
// 线程 1:Alice 转 100 给 Bob
std::thread t1([&]() {
for (int i = 0; i < 10; ++i) {
transfer(alice, bob, 10);
}
});
// 线程 2:Bob 转 100 给 Alice
// 注意:这里传递参数的顺序与 t1 相反
// 如果使用手动 lock(lock1, lock2) 且不加小心,这里极易死锁
// std::scoped_lock 会自动处理顺序问题
std::thread t2([&]() {
for (int i = 0; i < 10; ++i) {
transfer(bob, alice, 10);
}
});
// 等待线程完成
t1.join();
t2.join();
std::cout << "Alice final balance: " << alice.get_money() << "\n";
std::cout << "Bob final balance: " << bob.get_money() << "\n";
return 0;
}
在上述代码中,线程 1 尝试以 (alice, bob) 的顺序加锁,线程 2 尝试以 (bob, alice) 的顺序加锁。std::scoped_lock 内部通过算法避免了这种顺序差异导致的死锁。
4. 对比与选择
为了帮助你在不同场景下做出选择,以下是几种锁机制的对比如下:
| 特性 | 手动多次调用 lock() | std::lock + std::lock_guard | std::scoped_lock (C++17) |
|---|---|---|---|
| 死锁安全性 | 依赖人工顺序,不安全 | 安全(std::lock 保证) | 安全(内部调用类似算法) |
| 代码简洁性 | 低(需手动处理异常) | 中(需两行代码) | 高(一行代码搞定) |
| RAII 支持 | 需手动编写 try-catch | 支持(配合 adopt_lock) | 原生支持 |
| 适用版本 | C++11 | C++11 | C++17 及以上 |
| 推荐程度 | 不推荐 | 兼容旧代码时推荐 | 强烈推荐 |
5. 关键注意事项
- 避免重复锁定:不要对同一个互斥量对象在同一个
std::scoped_lock中传入两次,这会导致未定义行为(通常是死锁)。 - 作用域控制:
std::scoped_lock的生命周期决定了锁的持有时间。确保锁的持有时间尽可能短,不要在锁内执行耗时操作(如 I/O 或繁重的计算)。 - 移动语义:
std::scoped_lock是不可移动、不可拷贝的。它必须在其定义的作用域内使用。 - 参数类型:传入参数必须满足“基本互斥量”要求,如
std::mutex、std::recursive_mutex、std::shared_mutex等。
6. 进阶:std::adopt_lock 选项
在某些极少数情况下,你可能已经手动调用了 lock(),但希望使用 RAII 机制来管理解锁。std::scoped_lock 同样支持 std::adopt_lock 参数。
mutex1.lock();
mutex2.lock();
// 告知 scoped_lock 锁已经被持有,只需负责在析构时解锁
std::scoped_lock lock(std::adopt_lock, mutex1, mutex2);
但在绝大多数新代码中,直接让 std::scoped_lock 处理加锁是更优的选择。
通过以上步骤,你已经掌握了如何在 C++ 中使用 std::scoped_lock 来编写安全、简洁且无死锁风险的多线程代码。

暂无评论,快来抢沙发吧!