导读:本期聚焦于小伙伴创作的《C++17里怎么用std::scoped_lock一把锁住多个互斥量避免死锁》,敬请观看详情。写过多线程C++程序的工程师多半遇到过这样的麻烦:函数里要同时操作两个共享队列,分别加锁就得操心顺序,稍不留神就死锁。C++17给出的std::scoped_lock正是为解决这类问题设计的,它能在构造时以无死锁算法一次性锁住任意数量的互斥量,离开作用域自动释放。和老式的std::lock_guard不同,scoped_lock支持可变参数模板,接收多个互斥量引用后内部调用std::lock完成死锁避免,再逐一个包装。实际项目中把账户转账、双缓冲切换等需要多资源互斥的场景交给它,代码既短又安全。下文通过具体例子说明其写法、原理以及与手动std::lock加锁的差别。

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

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

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。