在C++17标准之前,如果一段逻辑需要同时访问被不同互斥量保护的资源,开发者往往得用std::lock搭配std::lock_guard,或者小心翼翼地按固定顺序加锁。这种做法不仅啰嗦,还容易因为顺序不一致引发死锁。C++17引入的std::scoped_lock让这件事变得简单且安全:它能在构造时一次性锁住多个互斥量,并以死锁避免算法保证不会卡住,析构时自动全部释放。

一、std::scoped_lock基本用法
std::scoped_lock定义在头文件<mutex>中,它是一个可变参数模板类。你可以把任意数量的互斥量引用传给它的构造函数,它会调用std::lock把这些互斥量全部锁住,然后用一种类似lock_guard的机制在析构时释放。下面的例子展示了如何用它对两个std::mutex同时加锁。
#include <mutex>
#include <iostream>
#include <thread>
std::mutex mtx_a;
std::mutex mtx_b;
int resource_a = 0;
int resource_b = 0;
void update_resources(int x, int y) {
// 构造时同时锁住mtx_a和mtx_b,离开作用域自动解锁
std::scoped_lock lock(mtx_a, mtx_b);
resource_a += x;
resource_b += y;
std::cout << "a=" << resource_a << ", b=" << resource_b << std::endl;
}
int main() {
std::thread t1(update_resources, 1, 2);
std::thread t2(update_resources, 3, 4);
t1.join();
t2.join();
return 0;
}
在上面代码中,我们并没有手动指定先锁mtx_a还是mtx_b。std::scoped_lock在内部通过std::lock实现了死锁避免:标准库会采用一种可避免循环等待的算法,确保多个线程以不同顺序请求这两个锁时也不会互相等待。对于只需要锁一个互斥量的场景,scoped_lock也能像lock_guard一样工作,因此很多人直接用它替代lock_guard。
值得注意的是,传给scoped_lock的必须是互斥量对象本身的引用,不能是临时副本。由于它使用了可变参数模板,你可以传入两个、三个甚至更多互斥量,例如std::scoped_lock lk(m1, m2, m3, m4);,所有互斥量都会在构造期间被锁住,且只要其中一个加锁抛异常,已经锁住的也会被正确释放,保证不漏锁。
二、和手动std::lock写法的对比
在C++17以前,避免多互斥量死锁的常见写法是先调用std::lock,再分别用std::lock_guard并传入std::adopt_lock参数。这种方式能达到同样效果,但代码明显更长,也更容易写错。下面是用传统方式实现和前面等价的逻辑。
#include <mutex>
std::mutex mtx_a;
std::mutex mtx_b;
void old_style_update() {
std::lock(mtx_a, mtx_b); // 同时锁住,死锁避免由std::lock保证
std::lock_guard<std::mutex> ga(mtx_a, std::adopt_lock);
std::lock_guard<std::mutex> gb(mtx_b, std::adopt_lock);
// 操作共享资源
}
对比可见,传统写法要写三行核心代码,且std::adopt_lock这个标签容易被忘记或误用。一旦某个lock_guard忘了标adopt_lock,就会尝试再次加锁,导致未定义行为。而std::scoped_lock把这些细节封装起来,只暴露一个构造函数,既减少样板代码,也降低了人为出错的概率。
从性能角度看,std::scoped_lock底层就是基于std::lock和lock_guard式的RAII析构,所以运行时开销与传统写法基本一致。它带来的好处几乎都是编译期安全和代码可读性方面的,没有额外性能负担,因此在支持C++17的项目中应当优先采用。
三、实际工程中的注意点
虽然scoped_lock很好用,但实际使用仍有几点需要留意。首先,它锁住的是传入的那些互斥量本身,如果你在函数中又通过别的路径获取了其中某个互斥量的引用并再次加锁,依然可能死锁。其次,scoped_lock对象一般应定义为局部变量,这样依赖C++的作用域规则就能精确控制锁的生命周期,不要把它做成类成员去长期持有锁。
#include <mutex>
class Account {
public:
void transfer(Account& other, int amount) {
// 同时锁住两个账户的互斥量,避免转账死锁
std::scoped_lock lock(m_mutex, other.m_mutex);
m_balance -= amount;
other.m_balance += amount;
}
private:
std::mutex m_mutex;
int m_balance = 0;
};
上面的转账例子是scoped_lock最典型的应用:两个账户互相转账时,如果各自先锁自己再锁对方,就很容易死锁;用scoped_lock把两个m_mutex一起锁住,问题迎刃而解。如果项目需要兼容C++11或C++14,则只能用前面说的std::lock加adopt_lock方案,但新项目直接用scoped_lock即可。
最后提醒,std::scoped_lock并不负责业务层面的并发正确性,它只保证加锁解锁不出死锁、不漏释放。多个资源之间的操作是否具备原子性、是否满足业务不变式,仍需要开发者自己设计。把它当作底层工具,配合清晰的数据访问边界,才能写出健壮的多线程C++程序。
std::scoped_lock互斥量死锁避免修改时间:2026-08-11 12:18:35