C++ 并发与多线程学习笔记(六)线程同步 condition_variable条件变量 async future

顾名思义,条件变量就是一种让线程满足的条件。需要和互斥锁配合使用

官方文档:http://www.cplusplus.com/reference/condition_variable/condition_variable/

需求:有时候,我们需要多个线程能够同时就绪,再继续往下执行,或者多个线程需要使用有限的互斥量,然后再继续执行。使线程之间从某处开始同步执行。

std::condition_variable是一个类,所以我们需要有个它的对象来使用。

成员函数wait()和notify_one()

wait和notify_one有以下规则。
wait用来等待一个条件,调用方式为函数
wait(unique_lock, bool)
其中第二个参数可以可调用对象或lambda表达式来返回。
如果第二个参数返回值为true,wait直接返回,继续执行。
如果第二个参数的返回值是false,那么wait()将解锁互斥量,并阻塞到本行
阻塞直到其他某个线程调用notif_one时为止
如果wait()没有使用第二个参数,那么相当于默认使用false
当其他线程用notify_one将本wait唤醒后,该线程从此处继续执行
wait不断尝试重新获取互斥锁,如果获取不到,那么继续堵塞,但此时的阻塞等同于lock();
如果wait有第二个参数(lambda表达式),就判断这个值,如果false,则同上,解锁lock(),进入阻塞,等待notify调用
如果wait第二个参数返回为true,则成功继续执行(此时是锁着的)
如果wait没有第二个参数,wait返回,相当于true。

相当于在notify_one调用后,线程重新执行wait,并在这之前执行一个lock()。
参考流程如图所示:

在这里插入图片描述
注意解锁时机。

实例还是使用之前的消息队列的代码,但我们重写出队和入队的函数。

#include <iostream>
#include <mutex>
#include <condition_variable>
using namespace std;

class MsgQueue
{
public:
	void InMsgQueue()
	{
		for (int i = 0; i < 10000; ++i)
		{
			unique_lock<mutex> my_unique_lock1(my_mutex);
			msgQueue.push_back(i); 
			my_condition.notify_one(); //唤醒wait的线程
			//...其他代码
		}
		return;
	}
	void OutMsgQueue()
	{
		int command = 0;
		while (true)//循环执行,等待条件
		{
			unique_lock<mutex> my_unique_lock2(my_mutex);
			my_condition.wait(my_unique_lock2, [this] //lambda表达式就是一个可调用对象(函数)
			{
				if (!msgQueue.empty())
					return true;
				return false;
			});

			command = msgQueue.front();
			msgQueue.pop_front();
			my_unique_lock2.unlock();//及时解锁
			cout << "取出元素:" << command << endl;
		}
	}
private:
	list<int> msgQueue;//使用链表,头尾删除更高效
	mutex my_mutex;
	condition_variable my_condition; //条件变量
};


int main()
{
	MsgQueue queue;
	thread InQueueObj(&MsgQueue::InMsgQueue, &queue);
	thread OutQueueObj(&MsgQueue::OutMsgQueue, &queue);
	InQueueObj.join();
	OutQueueObj.join();
	cout << "主线程结束" << endl;
	return 0;
}

通过改造以后,比较之前我们的出队和入队函数(已优化):

bool isEnd = false;
void InMsgQueue()
{
	for (int i = 0; i < 1000; i++)
	{
		unique_lock<mutex> my_guard(my_mutex);
		msgQueue.push_back(i);
		cout << "插入元素" << i << endl;
	}
	isEnd = true;
}
void OutMsgQueue()
{
	for (int i = 0; i < 3000; i++)
	{
		if (!msgQueue.empty())
		{
			unique_lock<mutex> my_guard(my_mutex);
			cout << "取出元素" << msgQueue.front() << endl;
			msgQueue.pop_front();
		}
		else if (isEnd && msgQueue.empty())
		{
			return;
		}
		else
		{
			//消息队列为空
			cout << "无请求" << endl;
		}
	}
}

之前的出队函数主要执行的步骤在if (!msgQueue.empty())这一步,那函数的时间复杂度就几乎为O(n),n在实际运行的时候相当于无穷(因为需要不断检测),导致大量的CPU浪费在判空上面,使用条件变量后,只有入队函数执行后才会去执行判空之类的操作,时间复杂度为O(入队请求数)节约了系统开销。

注意:这里很容易存在一种情况,就是在获取锁的问题上,由于wait的性质,它拥有锁的能力不一定比其他直接能够lock的线程强,比如上面的例子中,很容易出现入队线程连续拥有了锁,往其中扔了好几个数据,而wait的线程只有老老实实的等着,虽然整体影响不大,但出队的速度会明显慢于入队的速度,(或者入队的速度慢于出队的速度),这种现象是存在的,这时候就要考虑项目容忍的时间。

通过和第四节的代码耗时比较,第四节的要更快一点,而且使用条件变量还需要写更多的东西去模拟实例。通过观察还发现,连续插入1000条数据的测试中,wait的线程执行速度远跟不上插入的线程,某次的试验如图所示:
在这里插入图片描述
另外,notify不一定对线程的唤醒每次都能成功,有可能wait正在干活呢?
多线程的环境是复杂多变的,我们要充分考虑各种情况。如果读者尝试过运行以上代码,会发现一个严重的问题,那就是没有给出跳出wait的办法。上面的代码,虽然在结束前能稳定运行,但随着最后一个元素取出后,入队线程已经执行完毕,主线程仍在等待出队线程执行完毕,而出队线程会无限卡在wait那里。如果读者自己再仔细深究condition_variable的使用,会发现可能没有那么轻松,上面很简单的几个操作都有很多隐患,更不要说在大型项目里错综复杂的逻辑。
个人建议:在没有把握的时候,慎重使用condition_variable。

notify_all()

看名字就很明显了,当我们有多个线程需要被唤醒的时候,notify_all就登场了。
上面的代码,使用wait的函数有一个明显的问题就是抢夺处理机的能力不强,导致输出速度慢于输入速度,解决问题的方法多种多样,要么限流,要么增加输出的吞吐量,一次取多个数据,或者用更多的线程来取数据。

async future

async是个函数模板,用来启动一个异步任务,启动一个异步任务后,返回一个future对象。
future对象里面含有线程入口函数所返回的结果。总的来说就是使用async和future来快速启动一个线程完成函数,并在future中获得结果。示例:

int threadFunc()
{
	cout << "thread start,thread id =" << this_thread::get_id() << endl;
	chrono::milliseconds dura(5000);
	this_thread::sleep_for(dura);
	cout << "thread end,thread id = " << this_thread::get_id() << endl;
	return 5;
}

int main()
{
	future<int> res = async(threadFunc);
	cout << res.get() << endl;	
	cout << "主线程结束" << endl;
	return 0;
}

get函数会有阻塞的作用,只有线程函数有了返回值后,get才会继续运行。
我们通过额外向async传递一个参数,该参数类型是std::launch类型(枚举类型),来达到一些特殊的目的。
std::launch::deferred:表示线程入口函数调用被延迟到std::future的wait()或者get()函数调用时才执行
std::launch::async,在调用async的时候就创建线程

思考:如果wait()或者get()没有被调用,那么线程会执行吗?经过测验,是不会的,甚至线程都没有被创建。async()函数本身就是默认使用的std::launch::async标记

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 数字20 设计师:CSDN官方博客 返回首页