C++11笔记-多线程-细说Future

Future

在前文C++11笔记-多线程-初识-async()和Future中,初次使用async()和Future来实现一个多线程的demo;
在上一文中讲到,class std::future<>允许你等待线程结束并获取其结果,可以获取到线程函数的返回值或者是一个异常;
这里将在学《C++标准库》时,把关于future的详细内容记录下来,方便以后工作学习使用;
在18.3.2 细说Future中,讲到future用来表现某一操作的成果(outcome):可能是一个返回值或是一个异常,但二者不会同时存在;这份成果被管理于一个shared state内,后者可以被async()或一个packaged_task或一个promise创建处理。这份成果也许尚未存在,因此future持有的也可能是“生成该成果”的每一件必要的东西;

接口

操作 效果
future f Default 构造函数,建立一个future,带着无效状态(invalid state)
future f(rv) Move构造函数,建立一个新的future,状态取自rv,并令rv状态失效
f.~future() 销毁状态也销毁*this
f = rv Move assignment;销毁f的旧状态,取rv状态填之,并令rv状态失效
f.valid() 如果f具备有效状态就获得true,然后你才可以调用以下各个成员函数
f.get() 阻塞(block)直到后台操作完成。它会迫使被推迟的线程(deferred thread)同步启动(start synchronously),产出结果(如果有的话)或发出异常,并令其状态失效
f.wait() 阻塞(block)直到后台操作完成。它会迫使被推迟的线程(deferred thread)同步启动(start synchronously)
f.wait_for(dur) 阻塞(block)dur时间段,或直到后台操作完成。但被推迟的线程(deferred thread)并不会被强制启动
f.wait_until(tp) 阻塞(block)直至到达时间点tp,或直到后台操作完成。但被推迟的线程(deferred thread)并不会被强制启动
f.share() 产生一个shared_future带有当前状态,并令f的状态失效

注意事项

1.如果future是被async()返回且其相关的task推迟,对它调用get()或者wait()会同步启动该task,但是wait_for()和wait_until()都不会令一个被推迟任务(deferred task)启动;
2.future的成果只能被取出一次。因此future可能处于有效(valid)或无效(invalid)状态:有效意味着“某一操作的成果或爆发的异常”尚未被取出;
get()只能被调用一次,因为get()会令future处于无效状态;
面对无效的future,调用其析构函数、move assignment操作符或valid()以外的任何操作,都会导致不可预期的行为。
3.get()的返回值:

情况一 如果它是void,get()获得的就是void,也就是之前说的“无物”
情况二 如果future的template参数是个reference类型,get()便返回一个reference指向返回值;
情况三 非上述两种情况,则get()返回返回值的一份copy,或是对返回值进行move assignment动作——取决于返回类型是否支持move assignment语义;

注意:get()的返回值取决于future<>的特化类型;
4.future既不提供copy构造函数也不提供copy assignment操作符,确保绝不会有两个object共享某一个后台操作的状态(state)。“将某个future object状态搬移至另一个”的唯一办法是:调用move构造函数或move assignment操作符;然而通过使用share_future object 也可以令后台任务的状态被共享,其share()会释出控制权;
5.如果调用析构函数的那个future是某一shared state的最后拥有者,而相关的task已启动但尚未结束,析构函数会造成阻塞(block),直到任务完成;

处理后台异常

如果 std::async 调用的函数抛出异常,那么这个异常会被存储在值的位置,同时 future 变为 ready ,如果调用 get() 会重新抛出存储的异常。
在《C++标准库》18.1节中的处理异常部分中举了一个栗子:启动一个后台任务,该任务使用无限循环持续的分配内存给list 变量v增加新元素,这样线程会出现内存分配异常(bad_alloc);
给出以下代码:

#include <future>
#include <list>
#include <iostream>
#include <exception>

using namespace std;

void task1()
{
    list<int> v;
	while (true)
	{
		for (size_t i = 0; i < 1000000; i++)
		{
			v.push_back(i);
		}
		cout.put('.').flush();
	}

	cout << "task 1 over " << endl;
}
int main()
{
	cout << "starting 2 task" << endl;
	cout << "- task1: process endless loop of memory consuption" << endl;
	cout << "- task2: wait for <return> and then for task1" << endl;

	auto f1 = async(task1);
	//cin.get();
	cout << "\nwait for the end of task1:" << endl;
	try
	{
		f1.get();
	}
	catch (const exception& e)
	{
		cout << "EXCEPTION: " << e.what() << endl;
	}
    system("pause");
}

执行后效果:
C++11笔记-多线程-细说Future
当async()开始启动task1后,task1函数会有两种情况出现;
一种情况是:好的消息,没有什么特殊的事情发生;
另外一种情况是:坏的消息,“对future调用get()”也能处理异常;事实上,当get()被调用,且后台操作已经(或随后由于异常)而终止,该异常不会在此线程内被处理,而是会再次被传播出去。因此,欲处理后台操作所生的异常,你只需要偕同get()做出“以同步方式调用该操作”所作出的相同动作即可;
这个无限循环迟早会出现异常,该异常会终止线程,因为它未被捕获。Future object会保持这一状态直到get()被调用。搭配get()后这个异常在main()内被进一步传播;

等待wait/wait_for/wait_until

wait

只要对某个future调用wait(),就可以强制启动该future象征的线程并等待这一后台操作终止;

	std::future<...> f(std::async(func));
	...
	f.wait();

wait_for

使用wait_for()并给予一个时间段,就可让“异步、运行中”的操作等待一段有限时间:

std::future<...> f(std::async(func));
...
// 等待func线程10s;
f.wait_for(std::chrono::seconds(10));

wait_until

使用wait_until()就可以等待直至达到某特定时间点;

std::future<...> f(std::async(func));
...
// 等待func线程,直到1分钟后;
f.wait_until(std::system_clock::now() + std::chrono::minutes(1));

wait_for和wait_until并不强制启动线程(如果线程尚未启动的话);

wait_for和wait_until的返回值

不论wait_for和wait_until有以下三种返回值:
1.std::future_status::deferred——如果async()延缓了操作而程序中又完全没有调用wait()或get()(那会强制启动)。这种情况下上述两个函数都会立刻返回;
2.std::future_status::timeout——如果某个操作被异步启动但尚未结束,而waiting又已逾期(对给定的时间段而言);
3.std::future_status::ready——如果操作已经完成;
注意:如果给wait_for或者wait_until传入一个zero时间段或者一个过去的时间点,那么wait_for或wait_until函数仅仅是查询这个后台任务是否已被启动,和/或是否它正在运行中;

例子

《C++标准库》举了一个例子,显示了wait_until的应用场景;
需求:需要计算一个数值,计算时间或长或短,我们必须在某个时间段内获得一个粗精度的结果(quickComputation函数),如果这个时间段内能计算得到高精度的结果(accurateComputation)那就更好了;

int quickComputation();
int accurateComputation();
std::future<int> f;
int bestResultInTime()
{
	auto tp = std::chrono::system_clock::now() + std::chrono::minutes(1);
	f = std::async(std::launch::async, accurateComputation);
	int guess = quickComputation();
	std::future_status s = f.wait_until(tp);

	if (s == std::future_status::ready)
	{
		return f.get();
	}
	else
	{
		return guess;
	}
}

注意:future f不能是声明于bestResultInTime函数内的对象,那样的话若时间太短以至于无法完成accurateComputation(),future析构函数就会阻塞直到异步操作结束,这样bestResultInTime函数就会同时计算出粗精度结果和高精度结果,但返回值不一定是高精度的结果;

总结

async()提供了一种编程环境,让我们有机会并行启动某些“稍后(当get()被调用时)才会用到其结果”的动作。换句话说,如果你有某个独立函数f,你有可能受益于并行机制,做法是在你需要调用f时改而把f传给async(),然后在你需要f的结果时改为“对async()返回的future调用get()”;
future提供了外部对async()启动的线程的联系,可以获取到返回值或者异常,如果没有启动成功还能启动函数线程执行;

文献

1.《C++标准库》第二版;

上一篇:C++ 多线程使用future传递异常


下一篇:多线程的实现、CompletableFuture异步任务、@Async注解异步调用