C++11多线程(下)

九.条件变量 condition_variable ,notify_one , wait notify_all介绍

        接着上次的的测试函数继续往下看,我们仔细分析一下 isGet这个函数,就可以发现它的unique_lock<>()执行次数非常多,每次进入到 isGet 这个函数就会加锁,这样会拉低程序的运行效率,下面提供两个解决方案:

(1)双重锁定(双重检查)

        上一次我们在写单例类的时候已经说到过这个问题,双重检查会大大减少锁定的次数,提高程序的效率。

bool isGet(int &command)
{
	if (!l1.empty())//双重检查
	{
		unique_lock<mutex> guard(mymutex);
		if (!l1.empty())
		{
			command = l1.front();
			l1.pop_front();
			return true;
		}
	}
	return false;
}

        先对 l1 进行判空操作,如果l1是空,则就不会加锁,这样就会大大减少加锁的次数从而提高程序效率 。

(2)使用条件变量condition_varibale 

        condition_varibale 是一个类,使用它的wait()和notify_one()成员函数可以解决上述问题,wait()的第一个参数要与互斥量绑定,第二个参数是一个可调用对象 ,我们更多用lambda表示式来实现,当第二个参数返回false时,wait()是将拿到的锁释放掉,并将该线程卡在当前行,使其进入阻塞状态,只能在另一个线程中使用notify_one()才能将其唤醒;当第二个参数返回true的时,wait()直接返回,程序继续向下执行;当没有第二个参数时,效果和第二个参数返回false一样,等待另一个线程使用notify_one()将其唤醒。下面看我们是如何对int 和out这两个函数进行修改。

	void out()
	{
		int command = 0;
		while (1)
		{
			unique_lock<mutex> guard(mymutex);
			my_cond.wait(guard, [this]() {//lambda 表达式,this表示要调用类的对象(l1)
				if (l1.empty())return false;
					return true;
				});//lambda表达式

			command = l1.front();
			cout << "out函数执行了,取出一个元素" << command << endl;
			l1.pop_front();
			guard.unlock();
		}
	}

        对于out函数,我们修改后的代码如上,我们使用了wait(),并且我们用lambda表达式作为其第二个参数,其中的this表示要调用本类中的成员,此处就是要对l1进行判空,如果l1为空,那么我们就会将这个线程阻塞在wait()这行,out线程中的锁也因此会被释放,然后等待in线程中的notify_one()函数来将其唤醒。这里最后有一个unlock(),就是当线程完成它要完成互斥事务后,就可以对其进行提前解锁,来提高程序的效率,这也是unique_lock<>()的灵活性的体现,可以随时加锁解锁。

void in()
{
	for (int i = 0; i < 100000; i++)
	{
		my_cond.notify_one();
		unique_lock<mutex> guard(mymutex);
		cout << "in 函数执行了,插入一个元素" << i << endl;
		l1.push_back(i);
	}
}

        对于in函数,我们只需要加入一个notify_one()即可,专门用来唤醒out里面的wait().

        wait()函数会一直试着去拿锁,如果wait()函数拿到锁,如果其有第二个参数,并且第二个参数返回true,那么流程就会继续往下执行,此时在该线程中互斥量是被加锁的,直到该线程执行完毕,互斥量才会被解锁,第二个参数返回false,则线程就会被阻塞到改行;如果没有第二个参数,流程也会继续走下去。

        wait()和notity_one()函数的一些误解:这个两个线程并不是和表面上看上去的那样,in线程执行一次,out线程执行一次,实际上是在每一次in线程执行了notify_one()之后,in线程的unique_lock开始和out线程的wait开始抢夺这个互斥锁,谁抢夺成功了,那么那个线程就可以继续往下执行,所以说这个抢夺过程可能out线程的wait函数很久都抢不到一次互斥锁,因此这两个线程并不是依次执行的。如果out函数在对互斥量进行解锁之后还有其他的代码,而此时in线程执行到notify_one函数这里,这里并不会对wait起到唤醒作用,因为out线程在干别的事情,当且仅当out线程卡在wait这里,in线程执行notify_one才能够起到唤醒的作用,因此并不是每一次notify_one都能唤醒wait。

        运行结果很好的说明了notify_one和wait不是依次执行的。

        上面我们谈到的notify_one一次只能唤醒一个线程,但是在实际的操作中,可能会有多个线程需要被唤醒,这时候就需要引入notify_all()来唤醒所有的处于wait状态的线程。我们使用out再创建一个线程,那么在执行的时候就会让两个out线程都陷入wait状态,此时如果使用notify_one只能唤醒其中一个,另一个继续等待,因此同时需要将in函数中的notify_one改成notify_all,将两个wait都唤醒,但是此处只有一个互斥量,因此执行的时候还是只有一个wait能拿到锁,

十.async,future,package_task,promise介绍

        async是一个函数模板,用来启动一个异步任务,最后返回一个future对象,这个future对象里面含有线程入口函数的返回值,可以通过future的get成员函数得到返回值,get()函数会让程序卡在当前行直到线程执行完毕返回一个结果,主线程才能继续往下运行,当然在使用这两个类模板时候需要引入头文件<future>。

int mythread()
{
	cout << "mythread 开始执行,线程ID :" << this_thread::get_id() << endl;
	chrono::microseconds dura(5000);//休息5s
	this_thread::sleep_for(dura);
	cout << "mythread 执行完毕 线程ID:" << this_thread::get_id() << endl;
	return 6;
}
int main()
{
	cout << "主线程" << this_thread::get_id() << endl;
	future<int> result = async(mythread);
	cout << "**************" << endl;
	cout << result.get() << endl;//程序会卡在这一行
	cout << "Hello World" << endl;
	
	return 0;
}

        首先写一个线程入口函数,让其在运行的时候休息5s,便于观察,在主线程中我们通过asyns创建一个异步线程,用future来创建一个对象收其返回值,在通过get()函数返回拿到的结果,如果get()函数没有拿到返回值,那么就会卡在这里知道子线程执行结束。

        同时future对象也有一个wait()函数,其作用只是让主线程卡在这里等待,并不带回返回值,和thread里面的join()比较像。既然get()函数的功能如此强大,那么能否多次调用呢?答案是否定的

        我们在这里调用了两次get()函数,发现程序崩溃了,因此get()函数只能调用一次。

        下面我们把这个线程入口函数修改成类的成员函数,复习成员函数作线程入口函数如何创建线程:

class TEST
{
public:
	int mythread(int val)
	{
		cout << "mythread 开始执行,线程ID :" << this_thread::get_id() << endl;
		chrono::microseconds dura(5000);//休息5s
		this_thread::sleep_for(dura);
		cout << "mythread 执行完毕 线程ID:" << this_thread::get_id() << endl;
		return 6;
	}
};

int main()
{
	TEST T;
	int val = 5;
	cout << "主线程" << this_thread::get_id() << endl;
	future<int> result = async(&TEST::mythread,ref(T),val);
	cout << "**************" << endl;
	cout << result.get() << endl;//程序会卡在这一行
	cout << "Hello World" << endl;
	return 0;
}

        最重要的还是要注意成员函数作线程入口的创建方式,还有里面一些传参的方式。写到这里,有一个好奇的问题就是如果这个主线程里面既没有wait()有没有get()那么这个程序会如何执行呢,下面就让我们继续来测试一下:

        观察运行结果,我们发现即使没有wait()和get()函数,线程也是可以创建的 ,但是观察这个执行顺序不难发现,子线程是在主线程执行完才开始创建的。

        下面让我们来了解一下async的参数问题:在async的第一个参数中有一个枚举类型,launch,其包括下面俩成员(1)deferred (2)async ,首先介绍一下deferred这个,它的作用是推迟线程的创建,直到wait和get时才创建子线程,那么如果没有wait和get又会怎样呢,接着往下看;async参数就是直接在async函数这里创建线程,可见async函数的第一个参数默认使用的就是async。

         这里没有wait和get,我们使用了launch::deferred,子线程没有创建,运行结果里面没有子线程相关信息。

        通过对比wait和get的运行结果我们会发现,主线程id和子线程id是一样的,子线程是在主线程里被调用的,也就是说子线程和主线程是同一个线程,子线程压根就没有被创建 。下面再来测试一下async参数:

        通过观察三种情况下的运行结果发现,无论有没有wait还是get都会创建子线程。

        通过调试发现,确实是在future这里创建的子线程。说完这些下面让我们来看一下packaged_task.

        packaged_task是一个类模板,把各种可调用对象包装起来,方便作为线程入口函数;下面举个例子看一下。我们重新将线程入口函数写成一个普通的函数,用packaged_task将其打包:

	int main()
{
    cout << "主线程" << this_thread::get_id() << endl;
	packaged_task<int(int)> pthread(mythread);//将线程入口函数打包
	thread p1(ref(pthread), 1);//第一个参数是打包的线程入口函数,第二个参数就是线程入口函数的参数
	p1.join();
	future<int>result = pthread.get_future();
	cout << result.get() << endl;
	cout << "Hello World" << endl;
	return 0;
}

        main函数里面的package_tack的写法需要注意,里面的 int(int)表示的是一个函数的返回值是int,其有一个int的参数,用packaged_task将线程入口函数打包,再通过thread来创建线程,通过packaged_tack的get_future()函数来将线程的返回值和future创建的对象result绑定,再通过future的get函数拿到返回值。

        上面我们说到packaged_task可以将可调用对象进行绑定,那么我们就可以将上面的绑定换一种写法,我们可以使用lambda表达式来进行绑定。 

        packaged_task<int(int)> pthread([](int val) {
		cout << "mythread 开始执行,线程ID :" << this_thread::get_id() << endl;
		chrono::microseconds dura(5000);//休息5s
		this_thread::sleep_for(dura);
		cout << "mythread 执行完毕 线程ID:" << this_thread::get_id() << endl;
		return 6;
		});//将线程入口函数打包
	

        其他的都不需要修改,只需要将mythread改成lambda表达式就行。 

        程序一样可以正常运行。packaged_task本身也是一个可调用对象,当然此时就不会创建线程,就是一个函数调用。

	pthread(100);
	future<int>result = pthread.get_future();
	cout << result.get() << endl;
	cout << "Hello World" << endl;

 

        我们直接对打包好的线程进行传参调用,可以看到这里的线程id是相同的,所以这里就是一个函数调用。还有packaged_task放到容器中的写法:

    vector<packaged_task<int(int)>>pack;
	pack.push_back(move(pthread));
	packaged_task<int(int)>p;
	auto it = pack.begin();
	p= move(*it);
	pack.erase(it);
	p(123);
	//pthread(100);
	future<int>result = p.get_future();
	cout << result.get() << endl;
	cout << "Hello World" << endl;

        先创建一个packaged_task类型的容器,将刚才的打包好的可调用对象存入容器中,这里需要用到移动语义,不然会报异常,再将pthread移动到一个新的packaged_task对象中,在调用它;总之,写法有很多种,需要大家自己去探索。

        最后再来看一下 promise ,promise也是一个类模板,我们能够在一个 线程中给它赋值,然后在另外一个线程中取出这个值。

void accumalate(promise<int>& temp, int calc)
{
	int sum = 1;
	for (int i = 0; i < calc; i++)
	{
		sum += calc;
	}
	temp.set_value(sum);

}

        这是我们创建的线程入口函数,传入的第一个参数是一个promise对象,用来存放线程函数最后的返回结果,在这个函数里,我们简单的做一个加法运算,最后通过promise的set_value函数将最终的运算结果和promise创建的对象temp绑定。

	promise<int> my_promsie;
	thread mythread(accumalate, ref(my_promsie), 10);
	mythread.join();
	future<int>ful = my_promsie.get_future();
	cout << ful.get() << endl;
	cout << "Hello word" << endl ;

        在主函数里面,我们先创建一个promise对象用来作线程入口函数的参数,在通过thread创建线程,传入参数,将future和promise绑定,通过future的fet函数拿到最终的结果。

        注意:get依旧只能调用一次 。上面的写法是在主线程中拿到运算结果,我们还可以在新创建的线程中拿到运算结果。

void get_result(future<int>& temf)
{
	cout << "在线程中得到运算结果:" << temf.get() << endl;
}

        重新在写一个线程入口函数,这个函数里面只拿到它的运算结果,线程入口函数传入的是future对象,对于主函数,作以下更改:

thread mythread2(get_result, ref(ful));
mythread2.join();
cout << "Hello word" << endl ;

        需要将上面创建的future对象传入,然后不用写get函数,因为在新线程中已经get过了,再次get会出错,最终也会 得到一个正确的结果:

         这个promise配合future就可以实现线程的运算结果在线程之间传递。

十一.总结

        前面介绍了很多多线程相关的函数和一些相关的写法及用法,并不是所有的都必须掌握,在实际的开发中,只要能够用最简洁的代码写出高效的程序才是最重要的,不要把简单的问题复杂化,用好自己最熟悉的即可,遇到一些复杂的只要能看懂就行。

注:文章内容参考《C++新经典》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值