17.9 C++并发与多线程-async、future、packaged_task与promise

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++并发与多线程-补充知识、线程池浅谈、数量谈与总结

9.async、future、packaged_task与promise

  9.1 std::async和std::future创建后台任务并返回值

(1)std::async和std::future的用法

    以往的多线程编程中,用std::thread创建线程,用join来等待线程。
现在有一个需求,希望线程返回一个结果。当然,可以把线程执行结果赋给一个全局变量,这是一种从线程返回结果的方法,但是否有其他更好一点的方法呢?有,就是本节所讲的std::async和std::future
    std::async是一个函数模板,通常的说法是用来启动一个异步任务,启动起来这个异步任务后,它会返回一个std::future对象(std::future是一个类模板)。
    std::async所谓的启动一个异步任务,就是说std::async会自动创建一个新线程(有时不会创建新线程,后面会举例)并开始执行对应的线程入口函数。它返回一个std::future对象,这个对象里含有线程入口函数的返回结果。可以通过调用future对象的成员函数get来获取结果。
    future中文含义是“将来”,有人称std::future提供了一种访问异步操作结果的机制,就是说这个结果可能没办法马上拿到,但不久的将来等线程执行完了,就可以拿到(未来的值)。所以可以这样理解:future中会保存一个值,在将来某个时刻能够拿到。

int mythread()
{
    cout << "mythread() start" << " threadid = " << std::this_thread::get_id() << endl; //新的线程id
    std::chrono::milliseconds dura(20000); //1秒 = 1000毫秒,所以20000毫秒 = 20秒
    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); //流程并不会卡在这里,注意如果线程入口函数需要参数,可以把参数放在async的第二个参数的位置
	cout << "continue......!" << endl;
	cout << result.get() << endl;  //卡在这里等待线程执行完,但是这种get因为一些内部特殊操作,不能get多次,只能get一次,否则执行会报异常	
	cout << "main主函数执行结束!" << endl;
}

在这里插入图片描述
    上面通过std::future的get成员函数等待线程结束并返回结果。所以,future的get是很特殊的一个函数,不拿到值誓不罢休,程序执行流程必须卡在这里等待线程返回值为止。
    所以必须要保证,和std::future有关的内容一定要返回值或者一定要给result值,不然后续的result.get就会一直卡着。
    std::future还有一个叫wait的成员函数,这个成员函数只是等待线程返回,但本身不返回结果。读者可以把main主函数中的代码行“cout<<result.get()<<endl;”换成“result.wait();”试试。

result.wait(); //流程卡在这里等待线程返回,但本身不返回结果

    可以给async带参数,和前面讲解的std::thread里带的参数很类似,前面的范例看到的async只带一个参数(线程入口函数名)。如果用类的成员函数做线程入口函数,那async就要跟std::thread一样,看一看代码该怎样写。
    创建一个类A,可以直接把上面的mythread改造成类A的成员函数,然后给mythread加一个形参,并在最上面加一行代码输出形参值。

class A
{
public:
	int mythread(int mypar)
	{
		cout << mypar << endl;
		cout << "mythread() start" << " threadid = " << std::this_thread::get_id() << endl; //新的线程id
		std::chrono::milliseconds dura(20000); //1秒 = 1000毫秒,所以20000毫秒 = 20秒
		std::this_thread::sleep_for(dura); //休息一定的时长
		cout << "mythread() end" << " threadid = " << std::this_thread::get_id() << endl;
		return 5;
	}
};
{
	A a;
	int tmppar = 12;
	cout << "main" << " threadid = " << std::this_thread::get_id() << endl;
	//std::future<int>  result = std::async(&A::mythread, &a, tmppar); //这里第二个参数是对象地址,才能保证线程里面用的是同一个对象。第三个参数是线程入口函数的参数
	//auto  result = std::async(std::launch::deferred, &A::mythread, &a, tmppar); //这里注意,偷懒写法:auto
	//auto  result = std::async(std::launch::async, &A::mythread, &a, tmppar);
	//auto   result = std::async(std::launch::async | std::launch::deferred, &A::mythread, &a, tmppar); //“|”符号表示两个枚举值一起使用
	std::future<int>  result = std::async(&A::mythread, &a, tmppar);
	cout << "continue......!" << endl;
	cout << result.get() << endl;		
	cout << "main主函数执行结束!" << endl;
}

在这里插入图片描述

(2)std::async额外参数详解

    可以给async提供一个额外的参数,这个额外参数的类型是std::launch类型(一个枚举类型),来表示一些额外的含义。看一看这个枚举类型可以取哪些值。
  1.std::launch::deferred
    该参数表示线程入口函数的执行被延迟到std::future的wait或者get函数调用时,如果wait或者get没有被调用,则干脆这个线程就不执行了。修改main主函数中的async代码行:

auto  result = std::async(std::launch::deferred, &A::mythread, &a, tmppar); //这里注意,偷懒写法:auto

    如果后续调用wait或者get,则可以发现mythread线程入口函数被执行了,但同时也会惊奇地发现,认知中的async调用在这里根本没创建新线程,而是在主线程中调用的mythread线程入口函数,因为看下面的执行结果,子线程和主线程的线程id相同
在这里插入图片描述
  2.std::launch::async
    该参数表示在调用async函数时就开始创建并执行线程(强制这个异步任务在新线程上执行)。这意味着系统必须要创建出新线程来执行。

auto  result = std::async(std::launch::async, &A::mythread, &a, tmppar);

在这里插入图片描述
    观察上面的结果可以发现,会创建新的线程(主线程id和子线程id不同)。现在的情况是async调用后线程就创建并立即开始执行
  3.std::launch::deferred和std::launch::async。

auto result = std::async(std::launch::async | std::launch::deferred, &A::mythread, &a, tmppar); //“|”符号表示两个枚举值一起使用

在这里插入图片描述

    这里的两个枚举值用“|”连起来,是什么含义?这个含义千万不要理解错,这里非常容易理解错。这个“|”是或者的关系(参见11.1.2节的按位或运算),意味着async的行为可能是“创建新线程并立即开始执行线程”或者“没有创建新线程并且延迟到调用result.get或result.wait才开始执行线程入口函数(确切地说,这只是在主线程中调用线程入口函数而已)”,两者居其一
  4.不用任何额外的参数。

std::future<int>  result = std::async(&A::mythread, &a, tmppar);

    其实,这的效果和前面(3)中描述的效果完全相同。也就是说,如果std::async调用中不使用任何额外的参数,那么就相当于使用了std::launch::async|std::launch::deferred作为额外参数,这意味着系统自行决定是以同步(不创建新线程)或者异步(创建新线程)的方式运行任务。

(3)std::async和std::thread的区别

    std::thread这种创建线程的方式,如果这个线程返回一个值,程序员想拿到手也并不容易。例如,类似下面这样的代码用来创建一个普通线程

	std::thread mytobj(mythread);
	mytobj.join();

    那么这个时候就想到了std::async,它与std::thread不同,std::thread是直接的创建线程,而std::async其实是叫创建异步任务,也就是说std::async可能创建线程,也可能不创建线程。同时,std::async还有一个独特的优点:这个异步任务返回的值程序员可以通过std::future对象在将来某个时刻(线程执行完)直接拿到手。
    以下重点来了,由于系统资源的限制:
  (1)如果用std::thread创建的线程太多,则很可能创建失败,程序会报异常并且崩溃。
  (2)如果用std::async,一般就不会报异常崩溃,如果系统资源紧张导致无法创建新线程,std::async不加额外参数(或者额外参数是std::launch::async|std::launch::deferred)的调用就不会创建新线程而是后续谁调用了result.get来请求结果,那么这个异步任务就运行在执行这条get语句所在的线程上。也就是说,std::async不保证一定能创建出新线程来。如果程序员非要创建一个新线程出来,那就要使用std::launch::async这个额外参数,那么使用这个额外参数要承受的代价就是:当系统资源紧张时,如果非要创建一个新线程来执行任务,那么程序运行可能会产生异常从而崩溃。
  (3)根据经验来讲,一个程序(进程)里面创建的线程数量,如果真有非常大量的业务需求,则一般以100~200个为好,最高也不要超过500个。因为请不要忘记,线程调度、切换线程运行都要消耗系统资源和时间,读者日后可以依据具体的项目来测试创建多少个线程合适(所谓合适就是运行速度最快,效率最高)。

(4)std::async不确定性问题的解决

    std::async不加额外参数(或者额外参数是std::launch::async|std::launch::deferred)的调用,让系统自行决定是否创建新线程,存在了不确定性,这种不确定性可能会面对比较尴尬的无法预知的潜在问题。
    例如,如果系统自动决定延迟运行(std::launch::deferred),则意味着用std::async创建的任务不会马上执行。甚至如果不调用std::future对象的get或者wait方法,这个任务入口函数(这里称任务入口函数比称线程入口函数更合适)不会执行。
    这些潜在问题测试可能还测试不出来,因为只有计算机运行时间过长负荷太重的时候std::async无额外参数调用才会采用延迟调用策略。否则std::async一般都会创建线程来干活,因为创建线程是属于并行干活,效率肯定更高一些。

  9.2 std::packaged_task

    这是一个类模板,它的模板参数是各种可调用对象。通过packaged_task把各种可调用对象包装起来,方便将来作为线程入口函数来调用。

int mythread(int mypar)
{
	cout << mypar << endl;
	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::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::future对象里含有线程入口函数的返回结果,这里用result保存mythread返回的结果
	cout << result.get() << endl;
}

在这里插入图片描述
   如果要利用std::packaged_task包装一个lambda表达式可不可以呢?可以的。只需要在main主函数重新书写如下代码即可:

int main()
{
	cout << "main" << " threadid = " << std::this_thread::get_id() << endl;
	std::packaged_task<int(int)> mypt([](int mypar)
	{
		cout << mypar << endl;
		cout << "lambda mythread() start" << " threadid = " << std::this_thread::get_id() << endl;
		std::chrono::milliseconds dura(5000); //1秒 = 1000毫秒,所以5000毫秒 = 5秒
		std::this_thread::sleep_for(dura); //休息一定的时长
		cout << "lambda mythread() end" << " threadid = " << std::this_thread::get_id() << endl;
		return 15;
	});	
	std::thread t1(std::ref(mypt), 1);
	t1.join(); //可以调用这个等待线程执行完毕,不调用这个不行,程序会崩溃
	std::future<int> result = mypt.get_future();  //std::future对象里含有线程入口函数的返回结果,这里用result保存mythread返回的结果
	cout << result.get() << endl;
}

在这里插入图片描述
   当然,packaged_task包装起来的对象也可以直接调用。所以从这个角度来讲,packaged_task对象也是一个可调用对象。改造main主函数,改造后的代码如下:

int main()
{
	cout << "main" << " threadid = " << std::this_thread::get_id() << endl;
	std::packaged_task<int(int)> mypt([](int mypar)
	{
		cout << mypar << endl;
		cout << "lambda mythread() start" << " threadid = " << std::this_thread::get_id() << endl;
		std::chrono::milliseconds dura(5000); //1秒 = 1000毫秒,所以20000毫秒 = 20秒
		std::this_thread::sleep_for(dura); //休息一定的时长
		cout << "lambda mythread() end" << " threadid = " << std::this_thread::get_id() << endl;
		return 15;
	});
	mypt(105); //可调用对象,直接调用
	std::future<int> result = mypt.get_future();
	cout << result.get() << endl;
}

在这里插入图片描述
    在实际工作中,可能遇到packaged_task的各种用途,如放到容器中去,然后需要的时候取出来用。
    在main主函数的上面定义一个全局量:

vector<std::packaged_task<int(int)> > mytasks;

    在main主函数中,写入下面的代码:

int main()
{
	cout << "main" << " threadid = " << std::this_thread::get_id() << endl;
	std::packaged_task<int(int)> mypt([](int mypar) //创建或者叫包装一个任务
	{
		cout << mypar << endl;
		cout << "lambda mythread() start" << " threadid = " << std::this_thread::get_id() << endl;
		std::chrono::milliseconds dura(5000); //1秒 = 1000毫秒,所以20000毫秒 = 20秒
		std::this_thread::sleep_for(dura); //休息一定的时长
		cout << "lambda mythread() end" << " threadid = " << std::this_thread::get_id() << endl;
		return 15;
	});
	//入容器
	mytasks.push_back(std::move(mypt)); //移动语义,这里要注意,入进去后mytp就empty了
	//出容器
	std::packaged_task<int(int)> mypt2;
	auto iter = mytasks.begin();
	mypt2 = std::move(*iter); //用移动语义
	mytasks.erase(iter);//删除第一个元素,迭代器已经失效,不能再用
	mypt2(123); //直接调用
	//要取得结果,则还是要借助这个future
	std::future<int> result = mypt2.get_future();
	cout << result.get() << endl;
}

在这里插入图片描述
    前面介绍了一些基本的packaged_task用法。当然,用法本身就是多变的,读者以后可能会遇到各种各样的调用方式和各种奇怪的写法,但是有了前面讲解的这些基础,再辅以慢慢分析,在必要的情况下借助搜索引擎,相信理解各种写法的代码并非难事。

  9.3 std::promise

    这是一个类模板,这个类模板的作用是:能够在某个线程中为其赋值,然后就可以在其他的线程中,把这个值取出来使用。
    例如,创建一个线程,进行一个复杂的运算,这个运算大概需要好几秒钟,运算完毕了,需要知道运算结果,实现的方法有很多,包括前面讲解的get也能拿到。但这里介绍用promise来拿这个结果。直接看代码:

void mythread(std::promise<int>& tmpp, int calc) //注意第一个参数
{
	cout << "mythread() start" << " threadid = " << std::this_thread::get_id() << endl;
	//做一系列复杂操作
	calc++;
	calc *= 10;
	//做其他运算,整个花费了5秒
	std::chrono::milliseconds dura(5000);
	std::this_thread::sleep_for(dura);
	//终于计算出了结果
	int result = calc; //保存结果
	tmpp.set_value(result);  //结果保存到了tmpp这个对象中
	cout << "mythread() end" << " threadid = " << std::this_thread::get_id() << endl;
}
int main()
{
	cout << "main" << " threadid = " << std::this_thread::get_id() << endl;
	std::promise<int> myprom; //声明一个std::promise对象myprom,保存的值类型为int
	//创建一个线程t1,将函数mythread及对象myprom作为参数放进去
	std::thread t1(mythread, std::ref(myprom), 180);
	t1.join(); //等线程执行完毕,这个必须有,否则报异常,join放在.get后面也可以	
	//获取结果值
	std::future<int> fu1 = myprom.get_future(); //promise和future绑定用于获取线程返回值
	auto result = fu1.get(); //获取值,但是这种get因为一些内部特殊操作,不能get多次,只能get一次	
	cout << "result = " << result << endl;	
	//std::thread t2(mythread2, std::ref(fu1));
	//t2.join(); //等线程执行完毕
}

在这里插入图片描述
    总结起来,就是可以通过promise保存一个值,在将来的某个时刻通过把一个future绑到这个promise上来得到这个绑定的值。
    如果把上面代码中的join所在行注释掉,虽然程序会卡在“fu1.get();”行一直等待线程返回,但整个程序会报异常。当然,把join所在行放到get所在行之后也是可以的。总之:
  · join和get谁先出现,执行流程就会卡在其所在的行等待线程返回。
  · 程序中需要出现对join的调用,否则执行后程序会报异常。这一点读者可以自己测试。

void mythread2(std::future<int>& tmpf) //注意参数
{
	auto result = tmpf.get(); //获取值,只能get一次否则会报异常
	cout << "mythread2 result = " << result << endl;
	return;
}
int main()
{
	cout << "main" << " threadid = " << std::this_thread::get_id() << endl;
	std::promise<int> myprom; //声明一个std::promise对象myprom,保存的值类型为int
	//创建一个线程t1,将函数mythread及对象myprom作为参数放进去
	std::thread t1(mythread, std::ref(myprom), 180);
	t1.join(); //等线程执行完毕,这个必须有,否则报异常,join放在.get后面也可以	
	//获取结果值
	std::future<int> fu1 = myprom.get_future(); //promise和future绑定用于获取线程返回值
	//auto result = fu1.get(); //获取值,但是这种get因为一些内部特殊操作,不能get多次,只能get一次	
	//cout << "result = " << result << endl;	
	std::thread t2(mythread2, std::ref(fu1));
	t2.join(); //等线程执行完毕
}

在这里插入图片描述
    所以,感觉就是通过std::promise对象,实现了两个线程之间的数据传递。
当然这里只是简单的传递整型数据,其实数据的类型可以是各种各样的,有时间和兴趣可以自己尝试其他数据类型。

  9.4 小结

    读者也许会有这样一个疑惑:学习了这么多各种各样的多线程函数、对象,那么它们到底怎么用?什么时候用?
    其实,学这些东西并不是为了把它们都用在自己实际的开发中。相反,如果能用最少的知识和技巧写出一个稳定、高效的多线程程序,更值得赞赏。
    在程序员的成长道路上,阅读一些高手写的代码是非常必要的,从他们的代码中可以快速地实现自己代码库的积累(每个程序员都应该积累一套自己的代码库,里面的代码片段可以随时拿出来用于实际的开发工作中),技术也会有一个比较大幅度的提升。每个程序员都会遇到各种高手,他们写代码的习惯、风格和偏好也可能各不相同。所以在这里学习各种各样的多线程编程知识,笔者更愿意将学习的理由(目的)解释为:为将来能够读懂高手甚至大师写的代码而铺路。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值