17.10 C++并发与多线程-future其他成员函数、shared_future与atomic

17.1 C++并发与多线程-基础概念与实现
17.2 C++并发与多线程-线程启动、结束与创建线程写法
17.3 C++并发与多线程-线程传参详解、detach坑与成员函数作为线程函数
17.4 C++并发与多线程-创建多个线程、数据共享问题分析与案例代码
17.5 C++并发与多线程-互斥量的概念、用法、死锁演示与解决详解
17.6 C++并发与多线程-unique_lock详解
17.7 C++并发与多线程-单例设计模式共享数据分析、解决与call_once
17.8 C++并发与多线程-condition_variable、wait、notify_one与notify_all
17.9 C++并发与多线程-async、future、packaged_task与promise
17.10 C++并发与多线程-future其他成员函数、shared_future与atomic
17.11 C++并发与多线程-Windows临界区与其他各种mutex互斥量
17.12 C++并发与多线程-补充知识、线程池浅谈、数量谈与总结

10.future其他成员函数、shared_future与atomic

  10.1 std::future的其他成员函数

    现在把源代码恢复到上一节讲解async时的源代码:

int mythread()
{
	cout << "mythread() start" << " threadid = " << std::this_thread::get_id() << endl; //新的线程id
	std::chrono::milliseconds dura(5000); //1秒 = 1000毫秒,所以5000毫秒 = 5秒
	std::this_thread::sleep_for(dura); //休息一定的时长
	cout << "mythread() end" << " threadid = " << std::this_thread::get_id() << endl;
	return 5;
}
int main()
{
	cout << "main" << " threadid = " << std::this_thread::get_id() << endl;
	std::future<int>  result = std::async(mythread);
	cout << "continue......!" << endl;
	cout << result.get() << endl;  //卡在这里等待线程执行完,但是这种get因为一些内部特殊操作(移动操作),不能get多次,只能get一次
}

在这里插入图片描述
    实际上future还有很多方法,下面要调整main主函数中的代码,请读者认真阅读下面的代码和注释,因为其中包含着新知识,包括:
  · 判断线程是否执行完毕。
  · 判断线程是否被延迟执行(而且是通过主线程而非创建子线程来执行)。
    调整后的代码如下:

int main()
{
	cout << "main" << " threadid = " << std::this_thread::get_id() << endl;
	std::future<int>  result = std::async(mythread);
	//std::future<int>  result = std::async(std::launch::deferred,mythread); //流程并不会卡在这里
	cout << "continue......!" << endl;
	//cout << result.get() << endl;  //卡在这里等待线程执行完,但是这种get因为一些内部特殊操作(移动操作),不能get多次,只能get一次	
	//future_status看成一个枚举类型
	std::future_status status = result.wait_for(std::chrono::seconds(1));  //等待1秒,注意写法,但如果async的第一参数用了std::launch::deferred,则这里是不会做任何等待的,因为线程根本没启动(延迟)
	if (status == std::future_status::timeout)
	{
		//超时线程还没执行完
		cout << "超时线程没执行完!" << endl;
		cout << result.get() << endl; //没执行完这里也要求卡在这里等线程返回
	}
	else if (status == std::future_status::ready)
	{
		//线程成功返回
		cout << "线程成功执行完毕并返回!" << endl;
		cout << result.get() << endl;
	}
	else if (status == std::future_status::deferred)
	{
		//如果async的第一个参数被设置为std::launch::deferred,则本条件成立
		cout << "线程被延迟执行!" << endl;
		cout << result.get() << endl; //上节说过,这会导致在主线程中执行了线程入口函数
	}
}

  10.2 续谈std::async的不确定性问题

    在之前讲述了std::async不加额外参数(或者额外参数是std::launch::async|std::launch::deferred)的调用,会让系统自行决定是否创建新线程从而会产生无法预知的潜在问题。也谈到了问题的焦点在于如何确定异步任务到底有没有被推迟运行。
    这里只需要对10.1节中的代码做一点小小的改动,就可以确定异步任务到底有没有被推迟运行。改造main主函数,其中wait_for的时间给成0s即可。

int main(0)
{
	cout << "main start" << " threadid = " << std::this_thread::get_id() << endl;
	std::future<int>  result = std::async(mythread);
	std::future_status status = result.wait_for(std::chrono::seconds(0)); //可以写成0s,还支持ms(毫秒)写法					
	if (status == std::future_status::deferred)
	{
		cout << "线程被延迟执行!" << endl;
		cout << result.get() << endl; //可以使用.get,.wait()来调用mythread(同步调用),会卡在这里等待完成
	}
	else
	{
		//任务未被推迟,已经开始运行,但是否运行结束,则取决于任务执行时间
		if (status == std::future_status::ready)
		{
			//线程运行完毕,可以获取结果
			cout << result.get() << endl;
		}
		else if (status == std::future_status::timeout)
		{
			//线程还没运行完毕
			//......
		}
	}
}

在这里插入图片描述

  10.3 std::shared_future

    现在把源代码恢复到上一节讲解packaged_task时的源代码:

int mythread(int mypar)
{
	cout << "mythread() start" << " threadid = " << std::this_thread::get_id() << endl;
	std::chrono::milliseconds dura(5000); //1秒 = 1000毫秒,所以5000毫秒 = 5秒
	std::this_thread::sleep_for(dura); //休息一定的时长	
	return 5;
}
void mythread2(std::future<int>& tmpf) //注意参数
{
	cout << "mythread2() start" << " threadid = " << std::this_thread::get_id() << endl;
	auto result = tmpf.get(); //获取值,只能get一次否则会报异常
	cout << "mythread2 result = " << result << endl;
	return;
}
int main()
{
	cout << "main" << " threadid = " << std::this_thread::get_id() << endl;
	std::packaged_task<int(int)> mypt(mythread);  //把函数mythread通过packaged_task包装起来
	std::thread t1(std::ref(mypt), 1);  //线程直接开始执行,第二个参数作为线程入口函数的参数
	t1.join(); //调用这个等待线程执行完毕,不调用这个不行,程序会崩溃
	std::future<int> result = mypt.get_future();
	std::thread t2(mythread2, std::ref(result));
	t2.join(); //等线程执行完毕
}

在这里插入图片描述
    请回忆一下:用packaged_task把线程入口函数包装起来,然后创建线程mythread,用join等待线程执行结束,结束后线程的执行结果其实就保存在result这个future对象中了。然后启动线程mythread2,在该线程中把future对象(也就是result)作为参数传递到线程中,而后在线程中调用future对象的get函数,拿到了线程mythread的返回结果。整个程序的工作流程还是比较清晰的。上一节已经有过类似程序的演示了。
    但是需要说明的是,因为future对象的get函数被设计为移动语义,所以一旦调用get,就相当于把这个线程结果信息移动到result里面去了,所以再次调用future对象中的get就不可以了,因为这个结果值已经被移走了,再移动会报异常。那请想想,现在一个线程(mythread2)来get这个结果还好说,如果多个线程都需要用到这个结果,都去调用future对象的get函数,程序肯定报异常。
    那么,怎样解决这个问题呢?下面要讲的std::shared_future就上场了。
    std::shared_future和std::future一样,也是一个类模板。future对象的get函数是把数据进行转移,而shared_future从字面分析,是一个共享式的future,所以不难猜测到,shared_future的get函数应该是把数据进行复制(而不是转移)。这样多个线程就都可以获取到mythread线程的返回结果
    下面改造一下程序。首先改造main主函数,请注意阅读其中的代码和注释,都很重要:

void mythread2(std::shared_future<int>& tmpf) //注意参数
{
	cout << "mythread2() start" << " threadid = " << std::this_thread::get_id() << endl;
	auto result = tmpf.get(); //获取值,get多次没关系	
	cout << "mythread2 result = " << result << endl;
	return;
}
int main()
{
	cout << "main" << " threadid = " << std::this_thread::get_id() << endl;
	std::packaged_task<int(int)> mypt(mythread);
	std::thread t1(std::ref(mypt), 1);
	t1.join();
	std::shared_future<int> result_s(mypt.get_future()); //通过get_future返回值直接构造了一个shared_future对象
	auto mythreadresult = result_s.get();
	mythreadresult = result_s.get(); //可以调用多次,没有问题
	std::thread t2(mythread2, std::ref(result_s));
	t2.join(); //等线程执行完毕	
}

    执行起来,结果如下,一切正常:
在这里插入图片描述

  10.4 原子操作std::atomic

(1)原子操作概念引出范例

    在讲解原子操作之前,首先应该知道一个概念:如果有两个线程,即便是对一个变量进行操作,这个线程读这个变量值,那个线程去写这个变量值,哪怕这种读或者写动作只用一行语句。例如读线程,写线程,代码这样写:

	int tmpvalue = atomvalue;
	atomvalue++;

    上面的代码也会出现问题,读者可能认为读atomvalue值的时候,要么读到的是atomvalue被赋新值之前的老值,要么读到的是被赋值之后的新值,这个想法看起来是对的,但如果更深入地思考一下,事情也许并不如此简单。即便是一个简单的赋值语句操作,在计算机内部也是需要多个步骤来完成的。若对汇编语言比较熟悉,可能感触比较深,一般C++语言的一条语句会被拆解成多条汇编语句来执行。假设这里的自加语句(atomvalue++;)对应的是3条汇编语句来完成这个自加动作(相当于把自身值加1并将结果赋给自己),虽然这3条汇编语句每一条在执行的时候不会被打断,但这可是3条汇编语句,如果执行到第2条汇编语句被打断了,想象一下:修改一个变量的值,需要执行3条汇编语句,执行到第2条汇编语句时被打断(切换到另一个线程),那所赋值的这个变量里到底是什么值,就不好把握。

int g_mycout = 0;
void mythread()
{
	for (int i = 0; i < 10000000; i++) //1千万
	{
		g_mycout++; //对应的操作就是原子操作,不会被打断
	}
	return;
}
int main()
{
	cout << "main" << "threadid" << std::this_thread::get_id() << endl;
	thread mytobj1(mythread);
	thread mytobj2(mythread);
	mytobj1.join();
	mytobj2.join();
	cout << "两个线程都执行完毕,最终的g_mycout的结果是" << g_mycout << endl;
	cout << "main主函数执行结束" << endl;
	return 0;
}

在这里插入图片描述
    得到的结果基本都会比2000万小。这不难以想象,如果g_mycout要得到最终的正确结果,它每次自加1,都应该是上次+1顺利完成,中间不被打断作为基础。
    在C++11中,引入std::atomic来代表原子操作,这是一个类模板。这个类模板里面带的是一个类型模板参数,所以其实是用std::atomic来封装一个某类型的值。例如下面这行代码:

	std::atomic<int> g_mycout = 0;

在这里插入图片描述
    上面代码行就封装了一个类型为int的值,可以像操作int类型变量这样来操作g_mycout这个std::atomic对象。

(2)基本的std::atomic用法范例

    通过这个范例可以看到,这种原子类型的对象g_mycout,多个线程访问它时不会出现问题,赋值操作不会被从中间打断。
    上面的代码中,如果将“g_mycout++;”代码行修改为“g_mycout+=1;”效果会如何呢?请进行相应的代码修改:

g_mycout+=1; //对应的操作就是原子操作,不会被打断
g_mycout = g_mycout + 1; //这样写就不是原子操作了

    根据上面的结果得到一个结论:
    std::atomic并不是所有的运算操作都是原子的。一般来讲,包含++、–、+=、-=、&=、|=、^=等简单运算符的运算是原子的,其他的一些包含比较复杂表达式的运算可能就不是原子的。
    如果遇到一个表达式,其运算是否是原子的拿不准,则写类似上面的代码段来测试一下即可得到结论。
上面的范例针对的是原子int类型变量,再看一个小范例,原子布尔类型变量,其实用法也非常类似:

std::atomic<bool> g_ifend = false; //线程退出标记,用原子操作,防止读和写混乱

    mythread修改成如下的代码:

void mythread()
{
	std::chrono::milliseconds dura(1000);
	while (g_ifend == false) //不断的读
	{
		//系统没要求线程退出,所以本线程可以干自己想干的事情
		cout << "thread id = " << std::this_thread::get_id() << " 运行中......" << endl;
		std::this_thread::sleep_for(dura); //每次休息1秒
	}
	cout << "thread id = " << std::this_thread::get_id() << " 运行结束!" << endl;
	return;
}
int main()
{
	cout << "main" << " threadid = " << std::this_thread::get_id() << endl;
	thread mytobj1(mythread);
	thread mytobj2(mythread);
	std::chrono::milliseconds dura(5000);
	std::this_thread::sleep_for(dura);
	g_ifend = true; //对原子对象的写操作,让线程自行运行结束
	mytobj1.join();
	mytobj2.join();
}

在这里插入图片描述
    上面的程序代码非常简单,当主线程将g_ifend设置为true的时候,每个子线程判断到了g_ifend被设置为true从而跳出while循环并结束子线程自身的运行,最终等待两个子线程运行结束后,主线程运行结束,这意味着整个程序运行结束。
    另外,std::atomic有一些成员函数,都不复杂,但感觉用处不大,讲太多反而容易让人糊涂,如果生搬硬套地为了演示某个函数的功能来写一段没什么实际价值的测试代码,写出来后,即便读者知道这个函数的功能,但也不知道有什么实际用途,这种演示有还不如没有,无实际意义。如果日后真碰到这些成员函数,建议读者自行学习研究。

(3)笔者心得

    原子操作针对的一般是一个变量的原子操作,防止把变量的值给弄乱,但若要说这种原子操作到底有多大用处,读者需要在实际工作中慢慢体会。
    在笔者的实际工作中,这种原子操作适用的场合相对有限,一般常用于做计数(数据统计)之类的工作,例如累计发送出去了多少个数据包,累计接收到了多少个数据包等。试想,多个线程都用来计数,如果没有原子操作,那就跟上面讲的一样,统计的数字会出现混乱的情形,如果用了原子操作,所得到的统计结果数据就能够保持正确。
    有些读者对各种事物抱有好奇心,喜欢写出各种代码来做各种尝试,从研究的角度来讲,笔者支持这种尝试,但从实际的工作、写商业代码的角度来讲,建议谨慎行动。对于自己拿不准表现的代码,要么就写一小段程序来反复论证测试,要么就干脆不要使用,尤其是在商业代码中,只使用自己最有把握能够写好的代码,以免给自己服务的公司带来损失,尤其是对于缺人缺钱的小公司,商业代码一旦出错,这种损失可能对于公司是承受不起甚至是致命的。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值