在以往的 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 |
|
packaged_task
packaged_task 和 promise 在某种程度上有点像,只不过 promise 保存了一个共享状态的值,而 packaged_task 保存的是一个可调用对象。
它包含了两个最基本元素:
- 被包装的任务(stored task),一个可调用的对象,如函数指针、成员函数指针或者函数对象。
- 共享状态(shared state),用于保存任务的返回值,可以通过 future 对象来达到异步访问共享状态的效果。
1 | std::packaged_task<int()> task([](){ return 7; }); |
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 |
|
总结
C++11 本身提供了统一的跨平台的线程语法,在此基础上,future 和 promise、packged_task、async 进一步地简化了线程的异步操作,使得程序员从以前的多线程噩梦中解脱出来,而将更多的注意力放在具体的业务逻辑上,这是生产力的巨大进步。