C++11多线程(5):Future

C++11对多线程进行的支持,让我们可以直接跳过系统API直接在语言层面编写多线程程序,直接的好处就是程序的可移植性得到了很大的提高。这篇文章主要介绍future头文件中的相关内容。

正在进行C++11多线程编程相关的学习吗?点击这里查看这个分类的更多文章。

头文件future里主要包含多线程中用于异步访问变量的工具。例如,我们可以将耗时的计算放到其他线程,然后在未来的某个时间点访问计算结果,从而避免主线程的阻塞问题。借助future类和promise类,我们可以很轻松完成这些功能。

概念

在future头文件中有两个主要概念:ProvidersFuturesProviders是数据的提供方,Futures是数据的使用方。例如我们需要在线程A中进行运算,然后将结果传递给线程B,那么在这里,线程A就是Providers,线程B就是Futures

本文中涉及到一些重复的单词,为避免混淆,通过大小写来区分。例如Futures表示的是概念,future或者std::future表示具体的C++类型

ProvidersFutures一般是成对出现的,每一对ProvidersFutures实际包含同一块数据区域,我们称作shared-state,即共享数据状态。shared-state主要保存以下内容:

  • Providers需要传递给Futures的数据,Providers可以向Futures传递数据,也可以向Futures抛出一个异常。
  • shared-state本身的状态,例如未初始化,或者是就绪(ready)状态

std::promise 和 std::future

在头文件<future>中,最常用的类就是std::promisestd::future。在这里std::promise对象就是概念里提到的Providers,而std::future就是Futures

我们先来看下面这个示例:

#include <iostream>       // std::cout
#include <functional> // std::ref
#include <thread> // std::thread
#include <future> // std::promise, std::future

void print_int(std::future<int>& fut) {
int x = fut.get();
std::cout << "value: " << x << '\n';
}

int main()
{
std::promise<int> prom; // create promise

std::future<int> fut = prom.get_future(); // engagement with future

std::thread th1(print_int, std::ref(fut)); // send future to new thread

prom.set_value(10); // fulfill promise
// (synchronizes with getting the future)
th1.join();
return 0;
}

上面的代码涉及到两个线程,我们不妨成为主线程和副线程:

  • 主线程,主要执行main函数
  • 副线程,主要执行print_int函数,负责打印主线程传递过来的整型变量

主线程首先创建了std::promise对象,然后通过get_future方法获取与之对应的std::future对象。两个对象共享同一份shared-state数据。

std::future<int> fut = prom.get_future();

副线程拿到std::future对象,通过get函数来取得对应的结果。不过此时shared-state并不处于ready的状态,因此副线程在get方法内阻塞等待。

int x = fut.get();

主线程通过调用promset_value方法向shared-state中写入数据,并将其状态设置为ready,此时副线程从get方法中返回,获取到主线程设置的值(在代码中,这个值为10)。

prom.set_value(10);

看完了基础示例,我们来详细介绍每个Providers类型和Futures类型。

Providers

Providers类型主要包括std::promisestd::packaged_taskstd::promise我们在上面已经简单介绍过,而std::packaged_task本质上是对std::promise的封装,packaged_task可以让我快速的将一个普通的函数封装为一个支持promisefuture类型的函数,而不需要修改原来的函数。

接下来,我们会依次介绍std::promisestd::packaged_task

promise

主要构造函数

构造函数 说明
promise() 默认构造函数,构造一个空对象
promise (const promise&) = delete 拷贝构造函数(删除)
promise (promise&& x) noexcept 移动构造函数

和C++11其他多线程类型一样,promise对象允许移动构造(move),不允许拷贝构造(copy)。

promise::get_future

future<T> get_future();

获取对应的future对象,该future对象与当前promise对象指向同一个shared-state。因此,每个promise对象只能生成一个future对象,第二次调用get_future()函数会触发std::future_error::future_already_retrieved的异常。

一旦对一个promise对象使用了get_future()方法,那么这个promise对象就应该在未来某一个时刻“履行承诺”。可以有如下选择:

  • 调用set_value()方法来向Shared-state写入数据
  • 调用set_exception()方法来向Shared-state抛出异常,future中可以取得该异常。

一个没有按约定“履行承诺”的promise对象会在析构时向Shared-state抛出future_error类异常,相关的错误信息是broken_promise(无法履行的承诺)。

promise::set_value

void set_value (const T& val);
void set_value (T&& val);

Shared-state中写入变量val,并将Shared-state设置为ready

如果对应的future对象此刻正在get函数中等待结果,那么函数会继续运行并返回变量val

一个模板中比较特殊的例子,如果promise使用void来实例化模板,则set_value函数只会将Shared-state设置为ready,不会写入任何值。

set_value的使用可以本文开头的示例,这里不再重复。

promise::set_exception

void set_exception (exception_ptr p);

Shared-state中写入异常,并将Shared-state设置为ready

如果对应的future对象此刻正在get函数中等待结果,那么函数会返回并抛出异常。

#include <iostream>       // std::cin, std::cout, std::ios
#include <functional> // std::ref
#include <thread> // std::thread
#include <future> // std::promise, std::future
#include <exception> // std::exception, std::current_exception

void get_int (std::promise<int>& prom) {
int x;
std::cout << "Please, enter an integer value: ";
std::cin.exceptions (std::ios::failbit); // throw on failbit
try {
std::cin >> x; // sets failbit if input is not int
prom.set_value(x);
}
catch (std::exception&) {
prom.set_exception(std::current_exception());
}
}

void print_int (std::future<int>& fut) {
try {
int x = fut.get();
std::cout << "value: " << x << '\n';
}
catch (std::exception& e) {
std::cout << "[exception caught: " << e.what() << "]\n";
}
}

int main ()
{
std::promise<int> prom;
std::future<int> fut = prom.get_future();

std::thread th1 (print_int, std::ref(fut));
std::thread th2 (get_int, std::ref(prom));

th1.join();
th2.join();
return 0;
}

promise::set_value_at_thread_exit

void set_value_at_thread_exit (const T& val);
void set_value_at_thread_exit (T&& val);

将变量val保存到Shared-state中,但是并不立即将Shared-state的状态设置为ready,而是在当前线程退出后(在当前线程中所有储存资源都被销毁后)将其状态设置为ready

需要注意的是,对promise对象调用函数set_value_at_thread_exit后,Shared-state中就已经写入了具体的数值,在这之后如果继续调用set_value,会触发异常,异常类型为promise_already_satisfied

packaged_task

packaged_task可以让我快速的将一个普通的函数封装为一个支持promisefuture类型的函数,而不需要修改原来的函数。参考下面的代码:

#include <iostream>     // std::cout
#include <future> // std::packaged_task, std::future
#include <chrono> // std::chrono::seconds
#include <thread> // std::thread, std::this_thread::sleep_for

// count down taking a second for each value:
int countdown (int from, int to) {
for (int i=from; i!=to; --i) {
std::cout << i << '\n';
std::this_thread::sleep_for(std::chrono::seconds(1));
}
std::cout << "Lift off!\n";
return from-to;
}

int main ()
{
std::packaged_task<int(int,int)> tsk (countdown); // set up packaged_task
std::future<int> ret = tsk.get_future(); // get future

std::thread th (std::move(tsk),10,0); // spawn thread to count down from 10 to 0

// ...

int value = ret.get(); // wait for the task to finish and get result

std::cout << "The countdown lasted for " << value << " seconds.\n";

th.join();

return 0;
}

代码首先利用countdown函数构建了packaged_task对象,使之能够返回promise对象,然后将对应的函数托管到线程中执行,然后通过future::get来获取其结果。

除了可以用std::movepackaged_task对象中的函数托管到线程中运行,也可以直接调用operator()函数来在当前线程中执行对应的函数。

std::thread th(std::move(tsk), 10, 0); // 托管到其他线程中运行

tsk(10, 0); // 在当前线程中运行

主要构造函数

构造函数 说明
packaged_task() noexcept 默认构造函数,构造一个空对象
template <class Fn>
explicit packaged_task (Fn&& fn)
将函数fn封装为一个返回promise对象的函数
packaged_task (packaged_task&) = delete 拷贝构造函数(删除)
packaged_task (packaged_task&& x) noexcept 移动构造函数

packaged_task::make_ready_at_thread_exit
调用packaged_task中的函数,将结果保存到对应的Shared-state中,但是延迟到当前线程退出后再将Shared-state中的状态设置为ready

packaged_task::reset
保持packaged_task中设置的函数不变,但是分配一个新的Shared-state对象。旧的Shared-state对象(如果有)会被废弃,其效果等同于对原packaged_task调用了析构函数。

Futures

Future类型主要包括futureshared_futureshared_futurefuture的补充,借助shared_future,我们可以在多个地方访问Shared-state

future

future对象用来从某一类Providers(例如promisepackaged_task)中获取数据,如果对应的Provider处于不同的线程,future对象可以自动的处理线程同步的问题。

当我们说一个future对象是有效的(valid),也就是说该future对象和某一个Shared-state是关联的(associated)。一般来说,从以下途径获得的future对象是有效的:

  • std::async
  • std::promise::get_future
  • std::packaged_task::get_future

主要构造函数

构造函数 说明
future() noexcept 默认构造函数,构造一个空对象
future (const future&) = delete 拷贝构造函数(删除)
future (future&& x) noexcept 移动构造函数

对于程序来说,无效的future对象是无用的。使用默认构造函数产生的future对象是无效的。

future对象允许移动构造(move),不允许拷贝构造(copy),

future::get
如果当前future对象关联的Shared-state中状态为ready,那么返回其中保存的数值(或者异常)。

如果当前future对象关联的Shared-state中状态不为ready,那么当前线程会进入阻塞状态,直到Shared-state变为ready

一旦get函数成功返回(或者正确抛出异常),future对象会解除和当前Shared-state的关联,在此之后,当前future对象不再有效(valid)。因此对于每一个Shared-stateget函数只能调用一次。

future::valid
返回当前future是否有效。简单来说只有与Shared-state关联的future是有效的。

#include <iostream>       // std::cout
#include <future> // std::async, std::future
#include <utility> // std::move

int get_value() { return 10; }

int main ()
{

std::future<int> foo,bar;
foo = std::async (get_value);
bar = std::move(foo);

if (foo.valid())
std::cout << "foo's value: " << foo.get() << '\n';
else
std::cout << "foo is not valid\n";

if (bar.valid())
std::cout << "bar's value: " << bar.get() << '\n';
else
std::cout << "bar is not valid\n";

return 0;
}

输出:

foo is not valid
bar's value: 10

future::share
获取一个shared_future对象,该对象引用相同的Shared-state。与future不同的是,shared_future可以多次调用get函数,并且允许被拷贝,可以同时由多个线程持有。

需要注意的是,在调用share函数获取对应的shared_future后,原始的future会解除和Shared-state的关联,因此不再有效。

#include <iostream>       // std::cout
#include <future> // std::async, std::future, std::shared_future

int get_value() { return 10; }

int main ()
{

std::future<int> fut = std::async (get_value);
std::shared_future<int> shfut = fut.share();

// shared futures can be accessed multiple times:
std::cout << "value: " << shfut.get() << '\n';
std::cout << "its double: " << shfut.get()*2 << '\n';

return 0;
}

输出:

value: 10
its double: 20

future::wait

void wait() const;

阻塞等待,直到Shared-state变为ready

future::wait_for

template <class Rep, class Period>
future_status wait_for (const chrono::duration<Rep,Period>& rel_time) const;

阻塞等待,直到Shared-state变为ready,或者达到超时时间。

#include <iostream>       // std::cout
#include <future> // std::async, std::future
#include <chrono> // std::chrono::milliseconds

// a non-optimized way of checking for prime numbers:
bool is_prime (int x) {
for (int i=2; i<x; ++i) if (x%i==0) return false;
return true;
}

int main ()
{

// call function asynchronously:
std::future<bool> fut = std::async (is_prime,700020007);

// do something while waiting for function to set future:
std::cout << "checking, please wait";
std::chrono::milliseconds span (100);
while (fut.wait_for(span)==std::future_status::timeout)
std::cout << '.';

bool x = fut.get();

std::cout << "\n700020007 " << (x?"is":"is not") << " prime.\n";

return 0;
}

输出:

checking, please wait..........................................
700020007 is prime.

参考资料

Future - C++ Reference

此文有用? 求鼓励!

显示 Gitment 评论