C++并发与多线程(4)——unique_lock详解

一、unique_lock取代lock_guard

unique_locklock_guard灵活很多(多出来很多用法),但是效率差一点,内存的占用也会大一点。

cppreference对此的描述是:

A unique lock is an object that manages a mutex object with unique ownership in both states: locked and unlocked.

On construction (or by move-assigning to it), the object acquires a mutex object, for whose locking and unlocking operations becomes responsible.

The object supports both states: locked and unlocked.

This class guarantees an unlocked status on destruction (even if not called explicitly). Therefore it is especially useful as an object with automatic duration, as it guarantees the mutex object is properly unlocked in case an exception is thrown.

Note though, that the unique_lock object does not manage the lifetime of the mutex object in any way: the duration of the mutex object shall extend at least until the destruction of the unique_lock that manages it.

机翻:

unique_lock是管理互斥对象的对象,该对象在两种状态下都具有唯一所有权:锁定和解锁。

在构造时(或通过移动指定给它),对象获取一个互斥对象,对其执行锁定和解锁操作。

对象支持两种状态:锁定和解锁。

此类保证销毁时处于未锁定状态(即使未显式调用)。因此,作为具有自动持续时间的对象,它特别有用,因为它可以确保在抛出异常时正确地解锁mutex对象。

但是请注意,unique_lock对象不会以任何方式管理mutex对象的生存期:mutex对象的持续时间应至少延长到管理它的unique_lock被销毁为止。

一般格式:std::unique_lock<std::mutex> myUniLock(myMutex);

用以下之前的程序段作为例子:

class A {
public:
	void inMsgRecvQueue(void)
	{
		for (int i = 0; i < 10000; ++i)
		{
			myMutex.lock(); 
			//这一行可以不要,这样的话下面的adopt_lock参数要去掉
			unique_lock<mutex> myUnique(myMutex, adopt_lock);
			//这里替换了lock_guard,到目前为止看上去和lock_grand差不多
			cout << "inMsgRecvQueue()执行,插入一个元素" << i << endl;
			msgRecvQueue.push_back(i);
			//不用unlock,unique_lock是“智能锁”
		}
		return;
	}

private:
	list<int> msgRecvQueue;
	mutex myMutex;
};

下面详细解说unique_lock的参数。

二、unique_lock的第二个参数

1.std::adopt_lock

该参数在之前的笔记已经说过,它起到一个标记的作用,表示这个互斥量已经被lock(),即不需要在构造函数中lock这个互斥量了。

该参数标记的效果是:假设调用方线程已经拥有了互斥的所有权(即已经lock成功了),通知unique_lock不需要在构造函数中lock这个互斥量了。

使用前提:必须提前lock()。

一般的格式如下:

myMutex.lock(); 
unique_lock<mutex> myUnique(myMutex, adopt_lock);

或者

lock(myMutex); 
unique_lock<mutex> myUnique(myMutex, adopt_lock);

2.std::try_to_lock

该参数的作用是:尝试用mutex的lock()去锁定这个mutex,但如果没有锁定成功,会立即返回,不会阻塞在那里。 一般该参数都与owns_locks()搭配使用,owns_locks()方法判断是否拿到锁,如拿到返回true。

使用try_to_lock的原因是防止其他的线程锁定mutex太长时间,导致本线程一直阻塞在lock这个地方。

使用前提:不能提前lock()。

为了进一步说明该参数的作用,我们举一个简单的例子。该例程的出队线程在加锁互斥量后,出队线程休息5秒钟,这导致在这段时间内入队线程被阻塞,程序效率大大降低:

#include <iostream>
#include <list>
#include <thread>
#include <mutex>
#include <chrono>
using namespace std;

class A {
public:
	void inMsgRecvQueue(void)
	{
		for (int i = 0; i < 10000; ++i)
		{
			unique_lock<mutex> myUnique(myMutex); 
			cout << "inMsgRecvQueue()执行,插入一个元素" << i << endl;
			msgRecvQueue.push_back(i);
		}
		return;
	}

	void outMsgRecvQueue(void)
	{
		int command = 0;
		for (int i = 0; i < 10000; ++i)
		{
			unique_lock<mutex> myUnique(myMutex);
			chrono::milliseconds dura(5000); //1秒=1000毫秒,所以5秒=5000毫秒
			this_thread::sleep_for(dura); //线程休息5秒钟
			if (!msgRecvQueue.empty())
			{
				command = msgRecvQueue.front(); //代表执行了命令
				msgRecvQueue.pop_front();
				cout << "outMsgRecvQueue()执行,移除一个元素" << i << endl;
			}
			else {
				cout << "outMsgRecvQueue()执行,消息队列为空" << i << endl;
			}
		}
		return;
	}

private:
	list<int> msgRecvQueue;
	mutex myMutex;
};

int main()
{
	A myObja;
	thread myOutMsgObj(&A::outMsgRecvQueue, &myObja);
	thread myInMsgObj(&A::inMsgRecvQueue, &myObja);
	myOutMsgObj.join();
	myInMsgObj.join();
	return 0;
}

该程序的运行效果非常慢,我这里的测试结果是只有出队消息每隔5秒钟输出一次。如果我们将try_to_lock引入到本程序,程序效率将提升(这里只取出核心的一段):

	void inMsgRecvQueue(void)
	{
		for (int i = 0; i < 10000; ++i)
		{
			unique_lock<mutex> myUnique(myMutex, try_to_lock); //使用参数try_to_lock
			if (myUnique.owns_lock() == true) //如果拿到了锁,消息入队
				cout << "inMsgRecvQueue()执行,插入一个元素" << i << endl;
			else //如果没拿到锁,去做别的事
				cout << "inMsgRecvQueue()执行,干别的事" << i << endl;
			msgRecvQueue.push_back(i);
		}
		return;
	}

	void outMsgRecvQueue(void)
	{
		int command = 0;
		for (int i = 0; i < 10000; ++i)
		{
			unique_lock<mutex> myUnique(myMutex);
			chrono::milliseconds dura(5000); //1秒=1000毫秒,所以5秒=5000毫秒
			this_thread::sleep_for(dura); //线程休息5秒钟
			if (!msgRecvQueue.empty())
			{
				command = msgRecvQueue.front(); //代表执行了命令
				msgRecvQueue.pop_front();
				cout << "outMsgRecvQueue()执行,移除一个元素" << i << endl;
			}
			else {
				cout << "outMsgRecvQueue()执行,消息队列为空" << i << endl;
			}
		}
		return;
	}

这个程序的运行效果相当于在5秒钟的等待时间内,线程又去干别的事。这里的被锁时间比较夸大,因为事实上在开发过程中线程不会锁一个互斥量5秒钟之久,正常情况下如果不去强制线程sleep,一个线程执行互斥量代码的时间十分微小,但即使是微小的等待时间累计起来的浪费仍然不可小觑,因此这个参数十分重要。

3.std::defer_lock

作用: 如果unique_lock没有第二个参数,就会对mutex进行加锁,而加上defer_lock是初始化了一个没有加锁的mutex。

为什么要弄出这么一个参数呢?不给它加锁的目的是以后可以调用unique_lock的一些方法或者叫成员函数(因此,我们把这部分内容和下面的内容一起讲,这样会显得本部分内容比较少)。

使用前提:不能提前lock()。

二、unique_lock的成员函数

1.lock()和unlock()

如果程序员不想在定义unique_lock对象就给互斥量加上锁,那么就要在参数表中加入std::defer_lock,等到需要加锁时再引用方法lock(),这就是lock()的用处。

unique_lock是“智能锁”,在其作用域外就会自动解锁,那为什么还需要unlock()呢?因为有时候程序员不希望被锁的代码太多,又或者需要暂时解锁,因此引入了方法unlock(),增加了灵活性。例如:

{
    unique_lock<mutex> myUniLock(myMutex, defer_lock);
    myUniLock.lock();
    //处理一些共享代码
    myUniLock.unlock();
    //处理一些非共享代码
    myUniLock.lock();
    //处理一些共享代码
}

补充:lock()的代码段越少,执行越快,整个程序的运行效率越高。

锁住的代码少,叫做粒度细,执行效率高;

锁住的代码多,叫做粒度粗,执行效率低;

要学会尽量选择合适粒度的代码进行保护,粒度太细,可能漏掉共享数据的保护;粒度太粗,会影响程序的运行效率。所以,选择合适的粒度,是高级程序员的能力和实力的体现。

一个完整的例子如下:

void inMsgRecvQueue(void)
	{
		for (int i = 0; i < 10000; ++i)
		{
			unique_lock<mutex> myUnique(myMutex, defer_lock); //不加锁
			myUnique.lock(); //加锁
			cout << "inMsgRecvQueue()执行,插入一个元素" << i << endl;
			msgRecvQueue.push_back(i);
			myUnique.unlock(); //本条语句可有可无,引入unlock()方法是为了增加灵活性
		}
		return;
	}

2.try_lock()

该函数的作用是尝试给互斥量加锁,如果拿不到锁,返回false,否则返回true。下面是一个完整例子:

class A {
public:
	void inMsgRecvQueue(void)
	{
		for (int i = 0; i < 10000; ++i)
		{
			unique_lock<mutex> myUnique(myMutex, defer_lock); //不加锁
			if (myUnique.try_lock() == true) //返回true,表示拿到了锁
				cout << "inMsgRecvQueue()执行,插入一个元素" << i << endl;
			else //如果没拿到锁,去做别的事
				cout << "inMsgRecvQueue()执行,干别的事" << i << endl;
			msgRecvQueue.push_back(i);
		}
		return;
	}

	void outMsgRecvQueue(void)
	{
		int command = 0;
		for (int i = 0; i < 10000; ++i)
		{
			unique_lock<mutex> myUnique(myMutex);
			chrono::milliseconds dura(5000); //1秒=1000毫秒,所以5秒=5000毫秒
			this_thread::sleep_for(dura); //线程休息5秒钟
			if (!msgRecvQueue.empty())
			{
				command = msgRecvQueue.front(); //代表执行了命令
				msgRecvQueue.pop_front();
				cout << "outMsgRecvQueue()执行,移除一个元素" << i << endl;
			}
			else {
				cout << "outMsgRecvQueue()执行,消息队列为空" << i << endl;
			}
		}
		return;
	}

private:
	list<int> msgRecvQueue;
	mutex myMutex;
};

其实与try_to_lock和owns_lock()方法的效果一样的。有兴趣的同学,可以把上面延时部分的代码删掉,与没有try_lock的代码的执行效率做个比较,个人没有仔细比较过,但是显然前者的执行效率更快(前者是两个线程差不多同时到达9999,入队和出队线程是穿插进行的,显得非常繁忙;后者是两个线程到达9999的时间差的有点远,入队和出队不是穿插进行的,效率没那么快)。

3.release()

该成员函数的作用是:返回它所管理的mutex对象指针,并释放所有权;也就是说,unique_lock和mutex不再有任何关系。

需要严格区分release()和unlock()的区别:release是智能锁不再接管mutex,剩下的事务由程序员(你自己)管理;而unlock是智能锁的一个方法,仍然没有与智能锁脱离关系。

这就是说,如果原来mutex对象处于加锁状态且为智能锁接管,那么当release之后你来负责这个mutex并负责解锁。 下面是一个例子,一看就懂:

void inMsgRecvQueue(void)
	{
		for (int i = 0; i < 10000; ++i)
		{
			unique_lock<mutex> myUnique(myMutex); //加锁,相当于把myMutex和myUnique绑定在了一起
			mutex* ptr = myUnique.release(); //release()就是解除绑定,返回它所管理的mutex对象的指针,并释放所有权,后面都要手动操作了
			//所有权由ptr接管,如果原来mutex对象处理加锁状态,就需要ptr在以后进行解锁了
			cout << "inMsgRecvQueue()执行,插入一个元素" << i << endl;
			msgRecvQueue.push_back(i);
			ptr->unlock(); //需要自己手动解锁,如果没解锁,程序会终止崩溃
		}
		return;
	}

四、unique_lock所有权的传递

1.使用移动语义std::move

myUnique拥有myMutex的所有权,myUnique可以把自己对myMutex的所有权转移,但是不能复制。如下例子:

void inMsgRecvQueue(void)
	{
		for (int i = 0; i < 10000; ++i)
		{
			unique_lock<mutex> _myUnique(myMutex);
			//unique_lock<mutex> myUnique(_myUnique); 本条语句错误
			unique_lock<mutex> myUnique(move(_myUnique)); //本条语句正确,myUnique获得myMutex的所有权
			cout << "inMsgRecvQueue()执行,插入一个元素" << i << endl;
			msgRecvQueue.push_back(i);
		}
		return;
	}

2.返回临时对象mutex

unique_lock<mutex> func()
	{
		unique_lock<mutex> tmpmyUnique(myMutex);
		return tmpmyUnique;
		 //移动构造函数:从函数返回一个局部的unique_lock对象是可以的
        //返回这种局部对象会导致系统生成临时的unique_lock对象,并调用unique_lock的移动构造函数
	}

	void inMsgRecvQueue(void)
	{
		for (int i = 0; i < 10000; ++i)
		{
			unique_lock<mutex> myUnique = func();
			cout << "inMsgRecvQueue()执行,插入一个元素" << i << endl;
			msgRecvQueue.push_back(i);
		}
		return;
	}

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值