跟我学C++中级篇——std::scoped_lock

C++17的std::scoped_lock介绍

一、并行编程的同步

在并行和多线程编程中,一个难点在于多个锁的顺序处理问题。这不小心就有可能引起死锁,所以处理起来一定是慎之又慎。另外多个锁就引出另外一个问题,锁的控制粒度大小。而粒度的大小又可能明显的引起效率的变动。如果锁更多呢?某个锁的内部产生异常怎么办?当然这都有解决方法,但是不是看上去很复杂的样子。
有痛点就会有解决方案,C++17中提供了一个std::scoped_lock。

二、std::scoped_lock

std::scoped_lock,可以理解成域锁,原来是锁一个互斥体,现在是锁一片互斥体。它是采用了RAII机制的一种实现方式并可以通过std::lock的机制来避免死锁。它一下子解决了上面提到的不少的问题。进步从来不是一蹴而就。先看一下其定义:

  template<typename... _MutexTypes>
    class scoped_lock
    {
    public:
      explicit scoped_lock(_MutexTypes&... __m) : _M_devices(std::tie(__m...))
      { std::lock(__m...); }

      explicit scoped_lock(adopt_lock_t, _MutexTypes&... __m) noexcept
      : _M_devices(std::tie(__m...))
      { } // calling thread owns mutex

      ~scoped_lock()
      { std::apply([](auto&... __m) { (__m.unlock(), ...); }, _M_devices); }

      scoped_lock(const scoped_lock&) = delete;
      scoped_lock& operator=(const scoped_lock&) = delete;

    private:
      tuple<_MutexTypes&...> _M_devices;
    };

  template<>
    class scoped_lock<>
    {
    public:
      explicit scoped_lock() = default;
      explicit scoped_lock(adopt_lock_t) noexcept { }
      ~scoped_lock() = default;

      scoped_lock(const scoped_lock&) = delete;
      scoped_lock& operator=(const scoped_lock&) = delete;
    };

  template<typename _Mutex>
    class scoped_lock<_Mutex>
    {
    public:
      using mutex_type = _Mutex;

      explicit scoped_lock(mutex_type& __m) : _M_device(__m)
      { _M_device.lock(); }

      explicit scoped_lock(adopt_lock_t, mutex_type& __m) noexcept
      : _M_device(__m)
      { } // calling thread owns mutex

      ~scoped_lock()
      { _M_device.unlock(); }

      scoped_lock(const scoped_lock&) = delete;
      scoped_lock& operator=(const scoped_lock&) = delete;

    private:
      mutex_type&  _M_device;
    };

代码不复杂,一个标准的多参处理模板类,一个全特化的类,一个单独参数的模板类,用来分别处理不同的情况。这样看来写代码的确实是想的周到。

三、例程

看一个例程(cppreferenct.com):

#include <chrono>
#include <functional>
#include <iostream>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
using namespace std::chrono_literals;

struct Employee
{
    std::vector<std::string> lunch_partners;
    std::string id;
    std::mutex m;
    Employee(std::string id) : id(id) {}
    std::string partners() const
    {
        std::string ret = "Employee " + id + " has lunch partners: ";
        for (int count{}; const auto& partner : lunch_partners)
            ret += (count++ ? ", " : "") + partner;
        return ret;
    }
};

void send_mail(Employee&, Employee&)
{
    // Simulate a time-consuming messaging operation
    std::this_thread::sleep_for(1s);
}

void assign_lunch_partner(Employee& e1, Employee& e2)
{
    static std::mutex io_mutex;
    {
        std::lock_guard<std::mutex> lk(io_mutex);
        std::cout << e1.id << " and " << e2.id << " are waiting for locks" << std::endl;
    }

    {
        // Use std::scoped_lock to acquire two locks without worrying about
        // other calls to assign_lunch_partner deadlocking us
        // and it also provides a convenient RAII-style mechanism

        std::scoped_lock lock(e1.m, e2.m);

        // Equivalent code 1 (using std::lock and std::lock_guard)
        // std::lock(e1.m, e2.m);
        // std::lock_guard<std::mutex> lk1(e1.m, std::adopt_lock);
        // std::lock_guard<std::mutex> lk2(e2.m, std::adopt_lock);

        // Equivalent code 2 (if unique_locks are needed, e.g. for condition variables)
        // std::unique_lock<std::mutex> lk1(e1.m, std::defer_lock);
        // std::unique_lock<std::mutex> lk2(e2.m, std::defer_lock);
        // std::lock(lk1, lk2);
        {
            std::lock_guard<std::mutex> lk(io_mutex);
            std::cout << e1.id << " and " << e2.id << " got locks" << std::endl;
        }
        e1.lunch_partners.push_back(e2.id);
        e2.lunch_partners.push_back(e1.id);
    }

    send_mail(e1, e2);
    send_mail(e2, e1);
}

int main()
{
    Employee alice("Alice"), bob("Bob"), christina("Christina"), dave("Dave");

    // Assign in parallel threads because mailing users about lunch assignments
    // takes a long time
    std::vector<std::thread> threads;
    threads.emplace_back(assign_lunch_partner, std::ref(alice), std::ref(bob));
    threads.emplace_back(assign_lunch_partner, std::ref(christina), std::ref(bob));
    threads.emplace_back(assign_lunch_partner, std::ref(christina), std::ref(alice));
    threads.emplace_back(assign_lunch_partner, std::ref(dave), std::ref(bob));

    for (auto& thread : threads)
        thread.join();
    std::cout << alice.partners() << '\n'  << bob.partners() << '\n'
              << christina.partners() << '\n' << dave.partners() << '\n';
}

代码自己跑一下,没有什么特殊的。但要根据定义来和例程分析一下应用的过程,真正掌握,然后在工程上引入应用就会掌握。这里需要提醒就是scoped_lock支持的互斥体有很多(比如std::recursive_mutex等),所以如果是复杂情况的应用,千万要小心。

四、总结

技术的进步就是朝着简化开发者的应用的方向展开的。包括现在的AI,虽然说当下AI取代程序员可能有点忽悠的成分,但未来这种是大概率的事件。至少大多数的普通程序员被AI替代是一种非常可能的情况。这个时间可能不会很长。但这并不代表程序员的消失,反而有可能会出现另外一种形式的程序员。
大家拭目以待吧!

<think> 我们正在讨论的是C++中的`std::scoped_lock`,特别是`std::scoped_lock _l(mLock);`这一行代码的加锁范围。 根据C++标准,`std::scoped_lock`是一个RAII(Resource Acquisition Is Initialization)风格的锁管理类。它在构造时获取锁(一个或多个互斥量),在析构时释放锁。因此,其作用域就是它所在的作用域(通常是一个块作用域,比如一个函数体、一个循环体、或者一个显式的{}块)。 具体到代码行:`std::scoped_lock _l(mLock);` 它创建了一个名为`_l`的`std::scoped_lock`对象,并传入一个互斥量`mLock`(可能是`std::mutex`类型或类似的可锁定类型)。这个对象的生命周期决定了锁的持有期。 加锁范围: - 从该对象被创建的那一行开始,直到包含它的作用域结束(即遇到右花括号`}`时)。 - 当程序离开这个作用域(无论是通过正常执行到达作用域末尾,还是通过return、break、continue、goto等跳转语句,或者由于异常抛出)时,`_l`的析构函数会被调用,从而自动释放锁。 示例: ```cpp void some_function() { // ... 这里没有锁 { std::scoped_lock _l(mLock); // 从这里开始加锁 // 临界区开始 // ... 操作共享数据 // 临界区结束 } // 这里_l的作用域结束,自动释放锁 // ... 这里锁已经释放 } ``` 注意: - 如果`std::scoped_lock`用于多个互斥量(例如:`std::scoped_lock lk(mutex1, mutex2, mutex3);`),则它会在构造时同时锁定所有互斥量(采用避免死锁的算法),并在析构时按相反顺序释放这些锁。 - 在单个互斥量的情况下,它的行为类似于`std::lock_guard`,但`std::scoped_lock`更灵活,支持多个互斥量,因此C++17以后推荐使用`std::scoped_lock`。 引用说明: - 在C++标准中,`std::scoped_lock`被设计为在作用域内提供互斥量的锁定,从而保证线程安全[^1]。 因此,对于问题“std::scoped_lock _l(mLock); 的加锁范围”,我们可以明确回答:锁的范围是从对象`_l`的声明点开始,直到包含它的块作用域结束。 下面,我们将按照要求生成相关问题。 </think>### `std::scoped_lock` 的加锁范围详解 在 C++ 中,`std::scoped_lock _l(mLock);` 是一个基于 RAII(资源获取即初始化)的锁管理机制,其加锁范围由对象的生命周期决定: #### 加锁范围规则 1. **作用域绑定** ```cpp { std::scoped_lock _l(mLock); // 构造时加锁 ✅ // 临界区代码(受保护区域) ... } // 作用域结束自动解锁 ✅ ``` - 锁从构造点开始生效 - 锁在作用域结束时(对象析构时)自动释放 - 无论退出方式(正常结束、`return`、异常)都能保证解锁 2. **精确作用域控制** 可通过 `{}` 创建精确的临界区: ```cpp void process_data() { // 非临界区代码 ... { std::scoped_lock _l(mLock); // 进入临界区 shared_data.modify(); // 受保护操作 } // 离开临界区 // 其他非临界区代码 } ``` #### 关键特性 | 特性 | 说明 | |------|------| | **自动释放** | 离开作用域时自动调用析构函数解锁[^1] | | **异常安全** | 即使临界区抛出异常也能保证解锁 | | **零额外开销** | 比手动 `lock()`/`unlock()` 更安全高效 | | **支持多锁** | 可同时管理多个互斥量(如 `scoped_lock lk(m1, m2)`) | #### 对比其他锁管理方式 ```cpp // 危险的手动管理(可能忘记解锁) std::mutex mtx; mtx.lock(); // ❌ 可能因异常/return导致死锁 critical_section(); mtx.unlock(); // 安全的scoped_lock用法 { std::scoped_lock lk(mtx); // ✅ critical_section(); } ``` > **最佳实践**:始终将 `scoped_lock` 声明在最小必要的作用域内,减少锁的持有时间[^1]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值