c++ mutex 的包装类

本贴最后更新于 562 天前,其中的信息可能已经时移世改

mutex 的包装类

<mutex> 头文件中为我们提供了三种 mutex 包装类,分别是

  • lock_guard,提供了基于作用域的互斥锁包装类
  • unique_lock,提供了支持移动的互斥锁包装类
  • scoped_lock,支持多个互斥锁同时上锁避免死锁的互斥锁包装类

lock_guard

示例

lock_guard 基本的用法如下:

void Speaking::speak_lock_guard() { std::lock_guard<std::mutex> lock(m); speak_without_lock(); }

lock_guard 将所在的作用域视为临界区,由于 c++ 是块作用域,如果在一个函数中使用 lock_guard,则会将整个函数上锁,通常来说,为了只为临界区上锁,可以通过大括号构建出一个块作用域,如下:

void Speaking::speak_lock_guard() { std::cout << "not lock" << std::endl; { std::lock_guard<std::mutex> lock(m); speak_without_lock(); } }

原理剖析

我们来看一下 lock_guard 的源码,其原理一目了然:

/** @brief A simple scoped lock type. * * A lock_guard controls mutex ownership within a scope, releasing * ownership in the destructor. */ template<typename _Mutex> class lock_guard { public: typedef _Mutex mutex_type; explicit lock_guard(mutex_type& __m) : _M_device(__m) { _M_device.lock(); } lock_guard(mutex_type& __m, adopt_lock_t) noexcept : _M_device(__m) { } // calling thread owns mutex ~lock_guard() { _M_device.unlock(); } lock_guard(const lock_guard&) = delete; lock_guard& operator=(const lock_guard&) = delete; private: mutex_type& _M_device; };

其原理十分简单,在构造时使用 mutex 上锁,在析构函数中将其释放,这样就达到了在作用域范围内上锁的功能。

unique_lock

unique_lock 提供了通用互斥包装器,允许延迟锁定、锁定的有时限尝试、递归锁定、所有权转移和与条件变量一同使用。unique_lock 和 lock_guard 最大的区别在于支持移动语义,可以将某个锁的所有权移动到其他 unique_lock。

除了提供 mutex 的相关方法,还提供其他的一些方法,用于操作和观察当前 mutex 的状态。

// 修改器 void swap(unique_lock& u) noexcept; // 与另一 std::unique_lock 交换状态 mutex_type* release() noexcept; // 将关联互斥锁解关联而不解锁它 // 观察器 bool owns_lock() const noexcept; // 测试锁是否占有其关联的互斥锁 explicit operator bool () const noexcept; // 测试锁是否占有其关联的互斥锁 mutex_type* mutex() const noexcept; // 返回指向关联互斥锁的指针

原理剖析

我们来看一下移动语义是如何支持的:

explicit unique_lock(mutex_type& __m) : _M_device(std::__addressof(__m)), _M_owns(false) { lock(); _M_owns = true; } unique_lock(const unique_lock&) = delete; unique_lock& operator=(const unique_lock&) = delete; unique_lock(unique_lock&& __u) noexcept : _M_device(__u._M_device), _M_owns(__u._M_owns) { __u._M_device = 0; __u._M_owns = false; } unique_lock& operator=(unique_lock&& __u) noexcept { if (_M_owns) unlock(); unique_lock(std::move(__u)).swap(*this); __u._M_device = 0; __u._M_owns = false; return *this; } private: mutex_type* _M_device; bool _M_owns;

首先 unique_lock 在构造时,将关联的互斥锁的指针存在_M_device 中,并直接上了锁。

unique_lock 的拷贝构造函数和赋值运算符都是不可用的

移动构造函数中,直接将传入的右值引用中的互斥锁指针和 own 标识存下,并将传入的右值引用中的数据清空

移动赋值运算符中,首先将自己的锁 unlock,然后使用右值引用重新赋值了自己。

scoped_lock

scoped_lock 是提供便利 RAII 风格机制的互斥包装器,它在作用域块的存在期间占有一或多个互斥。

创建 scoped_lock 对象时,它试图取得给定互斥的所有权。控制离开创建 scoped_lock 对象的作用域时,析构 scoped_lock 并以逆序释放互斥。若给出数个互斥,使用免死锁算法。

scoped_lock 类不可复制。

示例

我们在构造时直接传入两个 mutex 对象,这样就会在 scoped_lock 的构造器中对两个锁同时进行上锁,当退出这个函数时,scoped_lock 被销毁,析构函数中会对这两个锁进行解锁。

void Speaking::speak_with_two_lock() { std::scoped_lock<std::mutex, std::mutex> scoped(m, m1); speak_without_lock(); }

原理剖析

我们来看看 scoped_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; };
  • 构造:当 scoped_lock 在构造时,首先将传入的参数的引用包裹为 tuple 保存到成员变量中,然后调用 std::lock 方法对多个互斥对象进行上锁,会采用死锁避免算法对其上锁,保证不会由于多个锁上锁顺序问题导致死锁。

  • 析构:在析构函数中,使用折叠表达式将传入的参数包调用 unlock 函数
    析构函数中定义了一个 lambda 表达式,它的参数是一个可变模板参数包,即 [](auto&... __m){ } 可以传入多个参数。在 lambda 表达式中,使用的折叠表达式去处理不定参数。折叠表达式是 C++17 中引入的,有四种形式,分别是:

    (... op x) (x op ...) (init op ... x) (x ... op init)

    其中,x 是不定参数,init 是个可以额外加入表达式的初始值,op 表示操作符,最终展开后如下:

    (x[1] op (... op (x[N-1] op x[N])))

    op 支持以下操作符:+ - * / % ^ & | = < > << >> += -= = /= %= ^= &= |= <<= >>= == != <= >= && || , . ->*。在二元折叠中,两个 op 必须相同

    小结

    本节分析了 c++ 中提供的几个互斥锁的包装类的用法和原理,希望大家在使用互斥锁进行分析时尽量不要使用 mutex 类进行上锁解锁操作,而是使用合适的包装类去解决问题。

  • C++

    C++ 是在 C 语言的基础上开发的一种通用编程语言,应用广泛。C++ 支持多种编程范式,面向对象编程、泛型编程和过程化编程。

    107 引用 • 153 回帖 • 1 关注
  • 线程
    123 引用 • 111 回帖 • 3 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...