C++使用unique_lock实现多重锁机制

  • Post author:
  • Post category:其他


提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档




前言

现代的计算机性能很强大,可以实现多线程并发。随之而来引入了另一个问题:“数据竞争”,这个问题是并发编程必须面对的。还有一种情形,当我们需要同时满足获取多个锁才能进行下一步操作该怎么办呢?

最简单的方法是定义3个mutex,依次lock(),当全部满足的时候再进行下一步。看起来似乎可行,但是对于场景比较复杂的情况可能引来一个非常危险的后果“死锁”:想想那样一种场景,你所需要的锁也同样被其他很多线程需要,随着锁的数量增加,“死锁”的概率随之增大,无形之中增加了风险。

也许你的编程能力非常强,但是这种隐藏起来的危害或许总有让你眼前一亮的时候!庆幸的是C++标准给我们提供了一个现成的方法,请往下看。




一、unique_lock是什么?

简而言之,unique_lock是C++标准为mutex包装的一个RAII风格(资源获取即初始化技术)对象,能够将“锁资源”自动管理起来,减少程序员的压力和出错的可能。



二、unique_lock使用



1.简单使用

代码如下(示例):

#include <iostream>
#include <mutex>

using namespace std;

void test_1(){
	mutex m;
    unique_lock<mutex> ul{m};
    //你的操作
}

上面的代码是最简单的用法,unique_lock对象创建的时候就会立刻获取锁,一旦拿到锁就可以进行你自己的操作了。不用手动调用lock(),一旦出了作用域就自动执行unlock()。

看下源代码片段:

      explicit unique_lock(mutex_type& __m)
      : _M_device(std::__addressof(__m)), _M_owns(false)
      {
	lock();
	_M_owns = true;
      }

lock()放在了构造函数里面,一旦获取到锁立即完成构造进行下一步。记着一个重要的变量:_M_owns,一会我们还会遇到。

看下析构函数:

      ~unique_lock()
      {
	if (_M_owns)
	  unlock();
      }

出了作用域unique_lock对象就自动析构了,顺便unlock()。这个地方有前提条件:注意刚才说的那个变量:_M_owns,只有它为true才会自动unlock(),如果是false,unlock()就不会自动调用了,这个后面再说。

简而言之,你通过最普通的那个构造函数(test_1())创建的unique_lock对象会自动上锁+解锁,全程不需要程序员参与,大多数场景下你用的是这个。



2.多重锁

终于还是到了这篇文章的重头戏,多重锁机制适合于一种更复杂的场景:你同时需要多个锁的排他权才能进行下一步。这里演示同时获取3个互斥锁的场景。

代码如下(示例):

#include <iostream>
#include <mutex>

using namespace std;

void test_2(){
    mutex m1;
    mutex m2;
    mutex m3;
    unique_lock<mutex> ul1{m1,defer_lock_t{}};
    unique_lock<mutex> ul2{m2,defer_lock_t{}};
    unique_lock<mutex> ul3{m3,defer_lock_t{}};
    lock(ul1,ul2,ul3);
    //你的操作
}

这里使用了双参数的构造函数,defer_lock_t的意思是延时加锁(暂不加锁)。

请看源代码片段:

      unique_lock(mutex_type& __m, defer_lock_t) noexcept
      : _M_device(std::__addressof(__m)), _M_owns(false)
      { }

仔细观察可以发现不同,跟上面的简单用法相比,函数体里面没有lock(),而且_M_owns初始化成false,记住这两个关键操作,后面会讲到。

所谓延时加锁的目的就是为了后面的那一步:统一加锁,也就是代码里的lock(ul1,ul2,ul3);这个函数不是unique_lock的成员函数,而是在这个头文件里面。

请看源代码片段:

  template<typename _L1, typename _L2, typename... _L3>
    void
    lock(_L1& __l1, _L2& __l2, _L3&... __l3)
    {
      while (true)
        {
          using __try_locker = __try_lock_impl<0, sizeof...(_L3) != 0>;
          unique_lock<_L1> __first(__l1);
          int __idx;
          auto __locks = std::tie(__l2, __l3...);
          __try_locker::__do_try_lock(__locks, __idx);
          if (__idx == -1)
            {
              __first.release();
              return;
            }
        }
    }

这个函数是可变参数模板,理论上支持不限制的锁,但是一般能同时使用3个以上的锁的场景并不会一直有。

多提一句,我上面写的test_2()函数有一个bug,细心的人可能已经发现了:只有lock()没有unlock(),原因很简单,使用多重锁必须程序员手动unlock(),因为_M_owns是false,所以unique_lock析构的时候不会调用unlock()。接下来我们完善下刚才的test_2()推出test_3()。

请看代码片段:

#include <iostream>
#include <mutex>

using namespace std;

void test_3(){
    mutex m1;
    mutex m2;
    mutex m3;
    unique_lock<mutex> ul1{m1,defer_lock_t{}};
    unique_lock<mutex> ul2{m2,defer_lock_t{}};
    unique_lock<mutex> ul3{m3,defer_lock_t{}};
    lock(ul1,ul2,ul3);
    //你的操作
    ul1.unlock();
    ul2.unlock();
    ul3.unlock();
}



总结

1、使用方法比较简单,使用RAII技术加持几乎不会出错



版权声明:本文为jiexijihe945原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。