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

发布于:2024-12-18 ⋅ 阅读:(58) ⋅ 点赞:(0)

一、并行编程的同步

在并行和多线程编程中,一个难点在于多个锁的顺序处理问题。这不小心就有可能引起死锁,所以处理起来一定是慎之又慎。另外多个锁就引出另外一个问题,锁的控制粒度大小。而粒度的大小又可能明显的引起效率的变动。如果锁更多呢?某个锁的内部产生异常怎么办?当然这都有解决方法,但是不是看上去很复杂的样子。
有痛点就会有解决方案,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替代是一种非常可能的情况。这个时间可能不会很长。但这并不代表程序员的消失,反而有可能会出现另外一种形式的程序员。
大家拭目以待吧!


网站公告

今日签到

点亮在社区的每一天
去签到