C ++ 多线程:条件变量、unique_lock

目录

1 前言

2 条件变量

3 unique_lock


1 前言

       为了更好的理解条件变量是什么,我们还是应当先思考一下为什么需要条件变量,还是先以一段程序为例:

using namespace std;

class MsgList   //模拟消息的写入和读取
{
public:
	void MsgWrite()
	{
		for (int i = 0; i < 10000; i++)   //写入1000条消息,用i来模拟消息
		{
			lock_guard guard(m);
			msgQue.push_back(i);
			cout << "Write Message : " << i << endl;
		}
	}
	void MsgRead()
	{
		while (true)    //用死循环来循环判断消息列表中是否有消息,如果有的话就将其读出
		{
			if (!msgQue.empty())
			{
				lock_guard guard(m);
				cout << "Read Message : " << msgQue.front() << endl; //读取最先来临的消息
				msgQue.pop_front();  //将已读消息删除
			}
			else
			{
				lock_guard guard(m);  //这里加锁是为了保证cout输出完整
				cout << "There is no Message !" << endl;
			}
		}
	}
private:
	deque msgQue;   //消息队列
	mutex m;   //互斥锁
};

int main()
{
	MsgList myMsg;
	thread ReadThread(&MsgList::MsgRead, &myMsg);  //写线程
	thread WriteThread(&MsgList::MsgWrite, &myMsg);   //读线程
	ReadThread.join();
	WriteThread.join();

    return 0;
}

        这里通过互斥锁实现了msgQue的读和写,运行结果自然是没问题的,不过,这并非是最好的。为什么不好呢?不好就不好在读线程函数是个死循环,程序会一直循环判断消息队列是否有消息,这样就造成了CPU不必要的开销。

        为什么会有这些不必要的开销呢?这是因为程序不知道到底消息队列中有没有消息,就一直需要去判断,如果能有一个好的办法,当消息队列中有消息的时候读线程再去执行,如果队列中没有消息的话就让读线程一直阻塞住。

        那么,该如何实现呢?这就是条件变量的作用了。

2 条件变量

      在C++11中,条件变量通过condition_variable类来实现。先来看一下condition_variable的解释:

C ++ 多线程:条件变量、unique_lock_第1张图片

       由此可知,条件变量实际上是condition_variable类的对象,通过这个对象可以实现对调用线程的阻塞。条件变量往往需要绑定一个unique_lock(也是互斥锁的一种实现),当在线程中通过条件变量调用wait函数时,该线程就会被阻塞住,直到在另一个线程中调用notify函数来唤醒这个线程。

       对于其中的wait函数,它提供两种重载方式,如下所示:

C ++ 多线程:条件变量、unique_lock_第2张图片  C ++ 多线程:条件变量、unique_lock_第3张图片

       这些描述有以下主要信息:

       ①wait函数会先对传入的unique_lock进行unlock,并且阻塞当前线程,unlock和block的操作,是一个原子行为。然后将当前线程添加到当前condition_variable对象的等待线程列表中;

       ②当唤醒线程调用notify_all或者notify_one时,就会解除wait线程的阻塞。解除阻塞之后,就会重新对unique_lock进行lock,然后wait函数返回;

       ③如果wait函数还传入了第二个参数pred,pred参数应当是个bool型的可调用对象。此时调用wait,会先判断第二个参数的返回值,如果返回false,就会执行第①步,否则直接返回;当被唤醒后再次判断第二个参数的返回值,如果返回false,会再次进入阻塞,否则直接返回;

       ④由于unique_lock的存在,保护了对可调用对象pred的访问。

       ⑤如果调用notify_all或者notify_one,实际上就是唤醒当前condition_variable对象的等待线程列表中的线程。

       那么如何在上述程序中使用条件变量呢?如下所示:

class MsgList
{
public:
	void MsgWrite()
	{
		for (int i = 0; i < 10000; i++)
		{
			unique_lock lck(m);
			msgQue.push_back(i);
			cout << "Write Message : " << i << endl;
			mycond.notify_one();   //唤醒wait
		}
	}
	void MsgRead()
	{
		while (true)
		{
			unique_lock lck(m);
			mycond.wait(lck, [this]() {   //调用wait函数,先解锁lck,然后判断lambda的返回值
				return !msgQue.empty();
			});
			int ReadData = msgQue.front();  //执行到这里,说明msgQue非空,就可以读取数据了
			msgQue.pop_front();
			cout << "Read Message : " << ReadData << endl;

		}
	}
private:
	deque msgQue;
	mutex m;
	condition_variable mycond;
};

 

3 unique_lock

       在使用条件变量的时候,涉及到了unique_lock,现在说一下unique_lock是什么东西。

       unique_lock与lock_guard相似,都能实现自动加锁和解锁,但是unique_lock比lock_guard使用更灵活,能实现更多的功能。unique_lock也是一个类,如上述代码中的unique_lock lck(m);就是创建了一个unique_lock对象lck,并将其与互斥量m绑定,同时对其上锁。

       与lock_guard不同的是,unique_lock可以进行临时解锁和再上锁,如在构造对象之后使用lck.unlock()就可以进行解锁,lck.lock()进行上锁,而不必等到析构时自动解锁。

       除此之外,unique_lock还接受第二个参数来进行构造。两个参数构造的形式有以下几种:

       unique_lock lck(m,adopt_lock):用互斥量来初始化unique_lock对象,但是构造时不会自动lock();

       unique_lock lck(m,defer_lock):仅仅是将lck与m绑定,不会自动进行lock()和unlock();

       unique_lock lck(m,try_to_lock):将lck与m绑定,并且尝试对其进行加锁,如果加锁失败也不会阻塞,加锁是否成功可以根据lck.owns_lock()来判断是否加锁成功;

 

        unique_lock还有一些常用的成员函数:lck.lock():对lck加锁;

                                                                      lck.unlock():对lck解锁;

                                                                      lck.try_lock():尝试锁,如果锁成功则返回true,否则返回false;

你可能感兴趣的:(C/C++)