我承诺会给你一个美好的未来

在以往的 C++98 中,多线程并行开发并不是那么容易的:你得小心翼翼地处理好「线程、锁、条件变量」的三角关系才能弄出个像样的多线程程序出来。

C++11 提供了 future 和 promise 来简化不同线程间的异步操作:

当一个任务需要向父线程(启动它的线程)返回值时,它把这个值放到 promise 中,之后,这个返回值会出现在和此 promise 关联的 future 中,于是父线程就能读到返回值。

在这种机制里,你只需要一个执行任务的线程,而不必再显示地使用其它的什么锁、条件变量等语义。

future

从字面意思看,它表示「未来」,通常我们不能立即获取到异步操作的执行结果,只能在未来某个时候获取。

一个有效的 future 对象通常由以下三种 provider 创建,并和某个共享状态相关联。

  • async 函数
  • promise::get_future
  • packaged_task::get_future

我们可以通过查询 future 的状态(future_status)来获取异步操作的结果。

future_status 有三种状态:

  • deferred:异步操作还没开始
  • ready:异步操作已经完成
  • timeout:异步操作超时

获取 future 结果有三种方式:

  • get 等待异步操作结束并返回结果
  • wait 只是等待异步操作完成,没有返回值
  • wait_for 超时等待返回结果

promise

promise 是个范型对象,可保存 T 类型的值,该值可被 future 对象在将来某个时刻读取。在构造 promise 时,promise 对象可以与共享状态关联起来,这个共享状态可以存储一个 T 类型或者一个由 std::exception 派生出的类的值,并可以通过 get_future 来获取与 promise 对象关联的对象,调用该函数之后,两个对象共享相同的共享状态(shared state)。

  • promise 对象是异步 provider,它可以在某一时刻设置共享状态的值。
  • future 对象可以返回共享状态的值,或者在必要的情况下阻塞调用者并等待共享状态标识变为 ready,然后才能获取共享状态的值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>  
#include <functional>
#include <thread>
#include <future>

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

int main ()
{
std::promise<int> prom;
std::future<int> fut = prom.get_future();
std::thread t(print_int, std::ref(fut));
prom.set_value(10);
t.join();
return 0;
}

packaged_task

packaged_task 和 promise 在某种程度上有点像,只不过 promise 保存了一个共享状态的值,而 packaged_task 保存的是一个可调用对象。

它包含了两个最基本元素:

  • 被包装的任务(stored task),一个可调用的对象,如函数指针、成员函数指针或者函数对象。
  • 共享状态(shared state),用于保存任务的返回值,可以通过 future 对象来达到异步访问共享状态的效果。
1
2
3
4
std::packaged_task<int()> task([](){ return 7; });
std::thread t1(std::ref(task));
std::future<int> f1 = task.get_future();
auto r1 = f1.get();

promise & future & packaged_task

promise 和 packged_task 都是异步 provider,都可以在将来某个时候设置与其关联的共享状态的值,它们内部都关联了一个 future 来异步访问共享状态的值。

稍微有点不同的是 packaged_task 包装的是一个异步操作、具体任务的返回值;而 promise 包装的是一个明确的、具体的值。

我的体会是:

需要直接读取线程函数中的某个值,就用 promise,需要获取异步操作的返回值,就用 packaged_task。

就这点细微的区别,我想用人类的语言解释一下。

比如,一个小伙子给一个姑娘表白真心的时候可能会说:「我承诺会给你一个美好的未来」或者「我会努力奋斗为你创造一个美好的未来」。

姑娘往往会说:「我等着」。

这三句话翻译成 C++11 语言就是:

小伙子说:「我承诺会给你一个美好的未来」等于 C++11 中 promise a future;

小伙子说:「我会努力奋斗为你创造一个美好的未来」等于 C++11 中 packaged_task a future;

姑娘梨花带雨地说:「我等着」等于 C++11 中 future.get()/wait();

小伙子两句话中的差异,自己琢磨一下,就是 promise 和 packaged_task 的差异,只可意会,不可言传吧。

现实中的山盟海誓靠不靠得住我不知道,但是 C++11 中的承诺和未来是一定可靠的,发起来了承诺就一定有未来,不管这个承诺是成功履行了(ready)或者出现了其它变故(exception)。

异步首选:async

C++11 还提供了异步接口 async,它会自动创建一个线程去执行线程函数,返回一个 future,这个 future 中存储了线程函数的返回值。

async 使我们可以在不显示调用线程的情况下就实现异步操作,获取异步执行状态和结果,真是 so easy,另外,它还提供了两种线程的创建策略。

1
async(std::launch::async | std::launch::deferred, f, args...)
  • std::launch::async:调用就开始创建线程,默认策略。
  • std::launch::deferred:延迟加载方式,调用时不创建线程,直到调用了 future 的 get 或者 wait 时才创建线程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream> 
#include <future>

// a non-optimized way of checking for prime numbers
bool is_prime (int x)
{
std::cout << "Calculating. Please, wait...\n";
for (int i=2; i<x; ++i) if (x%i==0) return false;
return true;
}

int main ()
{
std::future<bool> fut = std::async (std::launch::deferred, is_prime, 313222313);
std::cout << "Checking whether 313222313 is prime.\n";
bool ret = fut.get(); // waits for is_prime to return

if (ret) std::cout << "It is prime!\n";
else std::cout << "It is not prime.\n";

return 0;
}

总结

C++11 本身提供了统一的跨平台的线程语法,在此基础上,future 和 promise、packged_task、async 进一步地简化了线程的异步操作,使得程序员从以前的多线程噩梦中解脱出来,而将更多的注意力放在具体的业务逻辑上,这是生产力的巨大进步。

彦祖老师 wechat