提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
前言
现代的计算机性能很强大,可以实现多线程并发。随之而来引入了另一个问题:“数据竞争”,这个问题是并发编程必须面对的。还有一种情形,当我们需要同时满足获取多个锁才能进行下一步操作该怎么办呢?
最简单的方法是定义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技术加持几乎不会出错