9 - future

当线程结束执行时,不容易取回计算的结果。可以利用std::future获得线程运行的结果。

future简介

futurepromise中存储结果,即promise是结果的输入端,future是输出端。一旦在线程中运行的函数计算出希望返回的值,就把这个值放在promise中,最后通过future获取这个值。

可以使用future.get()获取结果,并将其保存在变量中。如果另一个线程尚未计算完结果,该调用将会阻塞,直到该结果值可用。只能在future上调用一次get(),因为第二次调用的行为是不确定的。

可先通过向future询问结果是否可用来避免阻塞:

if (myFuture.wait_for(0))
{
    int res = myFuture.get();
}
else
{
	// 值还拿不到    
}

promise与future

C++提供std::promise类,可在promise上调用set_value()来存储结果;也能用set_exception()来存储异常。需要注意的是,只能在特定的promise上调用set_xxx()一次,否则会抛出std::future_error()异常。

以下代码展示如何使用std::promisestd::future

void run(promise<int> promise)
{
	// 计算中...
	// 最终将结果存到promise里头
	promise.set_value(42);
}

int main()
{
	promise<int> myPromise;
	auto theFuture(myPromise.get_future());

	// 创建子线程进行计算, promise无法复制, 需要移动
	jthread theThread(run, std::move(myPromise));

	// 获取线程计算结果
	int res = theFuture.get();
	cout << "Res: " << res << '\n';
    
    // 如果是普通thread类, 别忘了join操作
    // theThread.join();
}

packaged_task

有了packaged_task将可以更方便地使用promise,而不是向上面那样要显式地使用promise。具体使用方法详见下面的代码:

int run(int a, int b) { return a + b; }

int main()
{
	// 创建一个packaged_task去跑run();
	packaged_task<int(int, int)> task(run);
	// 获取packaged_task的future
	auto theFuture(task.get_future());

	// 创建一个线程, 让它执行任务
	thread theThread(std::move(task), 39, 3);

	// 获取结果
	int res = theFuture.get();
	cout << "Res: " << res << '\n';

	theThread.join();

	return 0;
}

在上面的代码中,我们创建了一个packaged_task去执行run()。通过调用get_future(),从packaged_task 中检索future。启动线程,将packaged_task移入其中(packaged_task也无法被复制),等待结果并获取它。

通过使用packaged_task,省去我们手动声明promise并手动执行set_xxx(),很方便。

async()

如果想让操作系统决定是否创建一个线程以进行某种计算,可使用std::async()。它接收一个将要执行的函数,并返回可用于检索结果的future

async()可通过两种方法运行函数:

  • 创建一个新线程,异步运行提供的函数。
  • 在返回的future上调用get()方法时,在主线程上同步运行函数。

这两种方法通常是由操作系统选择的,当然也能手动指定策略参数:

  • launch::async:强制使用方法1。
  • launch::deferred:强制使用方法2。
  • launch::async | launch::deferred:默认行为,让操作系统自由选择两种方法。

使用例如下:

int run() { return 123; }

int main()
{
	auto myFuture(std::async(run));

	int result = myFuture.get();
	cout << result << '\n';

	return 0;
}

需要注意的是,调用async()锁返回的future会在其析构函数中阻塞,直到结果可用为止。看看下列代码:

std::async(run);

这里返回了一个临时future,将在该语句完成前就调用析构函数,在结果可用前,该析构函数将一直被阻塞

使用future处理异常

可以用future把子线程的异常放到主线程处理:

int run()
{
	throw std::runtime_error("I am ERROR\n");
}

int main()
{
	auto myFuture(std::async(std::launch::async, run));

	try
	{
		int result = myFuture.get();
	} catch (const std::exception& ex)
	{
		cout << ex.what();
	}
	
	return 0;
}

shared_future

future.get()只能调用一次。如果想多次调用get(),甚至从多个线程中多次调用,则需要使用std::shared_future<T>T是可拷贝构造的。使用std::future::share()或给shared_future构造函数传递std::move(future)以创建shared_future

shared_future可用于同时唤醒多个线程,例如下列代码:

promise<void> thread1Started, thread2Started;

promise<int> signalPromise;
shared_future<int> signalFuture(signalPromise.get_future().share());

auto func1 = [&thread1Started, signalFuture]
{
	thread1Started.set_value();
	// 等待参数被设定
	int parameter = signalFuture.get();
	cout << "Func1: " << parameter << '\n';
};

auto func2 = [&thread2Started, signalFuture]
{
	thread2Started.set_value();
	// 等待参数被设定
	int parameter = signalFuture.get();
	cout << "Func2: " << parameter << '\n';
};

// 异步运行两个lambda表达式, 记得获取future结果
auto res1 = std::async(launch::async, func1);
auto res2 = std::async(launch::async, func2);

// 等待两个线程开始
thread1Started.get_future().wait();
thread2Started.get_future().wait();

// 两个线程等待参数被设定, 设定参数后两个都被唤醒
signalPromise.set_value(42);

在上述代码中,首先定义了两个lambda表达式,它们在不同线程上异步执行。每个lambda表达式首先将值设置为自己的promise,以指示线程已启动。然后对signalFuture调用get(),这将一直阻塞,直到可通过future获得参数为止。两个lambda表达式都有shared_future<int> signalFuture的副本。主线程使用std::async()在不同线程上执行这两个lambda表达式,一直等待两个线程启动后,设置signalFuture的参数以唤醒这两个线程。

参考资料

  • C++20高级编程