Futures 是一种通过自然的、可组合的方式表达异步计算的模式。这篇博文介绍了我们在 Facebook 中使用的一种适用于 C++11 的 futures 实现:Folly Futures。
为什么要使用异步?
想象一个服务 A 正在与服务 B 交互的场景。如果 A 被锁定到 B 回复后才能继续进行其他操作,则 A 是同步的。此时 A 所在的线程是空闲的,它不能为其他的请求提供服务。线程会变得非常笨重-切换线程是低效的,因为这需要耗费可观的内存,如果你进行了大量这样的操作,操作系统会因此陷入困境。这样做的结果就是白白浪费了资源,降低了生产力,增加了等待时间(因为请求都在队列中等待服务)。
如果将服务 A 做成异步的,会变得更有效率,这意味着当 B 在忙着运算的时候,A 可以转进去处理其他请求。当 B 计算完毕得出结果后,A 获取这个结果并结束请求。
同步代码与异步代码的比较
让我们考虑一个函数 fooSync,这个函数使用完全同步的方式完成基本计算 foo,同时用另一个函数 fooAsync 异步地在做同样的工作。fooAsync 需要提供一个输入和一个能在结果可用时调用的回调函数。
template <typename T> using Callback = std::function<void(T)>;
Output fooSync(Input);
void fooAsync(Input, Callback<Output>);
这是一种传统的异步计算表达方式。(老版本的 C/C++ 异步库会提供一个函数指针和一个 void* 类型的上下文参数,但现在 C++11 支持隐蔽功能,已经不再需要显式提供上下文参数)
传统的异步代码比同步代码更为有效,但它的可读性不高。对比同一个函数的同步和异步版本,它们都实现了一个 multiFoo 运算,这个运算为输入向量(vector)中的每一个元素执行 foo 操作:
using std::vector;
vector<Output> multiFooSync(vector<Input> inputs) {
vector<Output> outputs;
for (auto input : inputs) {
outputs.push_back(fooSync(input));
}
return outputs;}
void multiFooAsync(
vector<Input> inputs,
Callback<vector<Output>> callback){
struct Context {
vector<Output> outputs;
std::mutex lock;
size_t remaining;
};
auto context = std::make_shared<Context>();
context->remaining = inputs.size();
for (auto input : inputs) {
fooAsync(
input,
[=](Output output) {
std::lock_guard<std::mutex> guard(context->lock);
context->outputs->push_back(output);
if (--context->remaining == 0) {
callback(context->outputs);
}
});
}}
异步的版本要复杂得多。它需要关注很多方面,如设置一个共享的上下文对象、线程的安全性以及簿记工作,因此它必须要指定全部的计算在什么时候完成。更糟糕的是(尽管在这个例子中体现得并不明显)这使得代码执行的次序关系(computation graph)变得复杂,跟踪执行路径变得极为困难。程序员需要对整个服务的状态机和这个状态机接收不同输入时的不同行为建立一套思维模式,并且当代码中的某一处不能体现流程时要找到应该去检查的地方。这种状况也被亲切地称为“回调地狱”。
Futures
Future 是一个用来表示异步计算结果(未必可用)的对象。当计算完成,future 会持有一个值或是一个异常。例如:
-
#include <folly/futures/Future.h>
-
using folly::Future;
-
// Do foo asynchronously; immediately return a Future for the output
-
Future<Output> fooFuture(Input);
-
Future<Output> f = fooFuture(input);
-
// f may not have a value (or exception) yet. But eventually it will.
-
f.isReady(); // Maybe, maybe not.
-
f.wait(); // You can synchronously wait for futures to become ready.
-
f.isReady(); // Now this is guarant