C++学习(四):Facebook 的 C++ 11 组件库 Folly Futures

article/2025/11/5 20:42:42

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 会持有一个值或是一个异常。例如:

 
  1. #include <folly/futures/Future.h> 

  2. using folly::Future;

  3.  
  4. // Do foo asynchronously; immediately return a Future for the output

  5. Future<Output> fooFuture(Input);

  6.  
  7. Future<Output> f = fooFuture(input);

  8. // f may not have a value (or exception) yet. But eventually it will.

  9. f.isReady();  // Maybe, maybe not.

  10. f.wait();     // You can synchronously wait for futures to become ready.

  11. f.isReady();  // Now this is guaranteed to be true.

  12. Output o = f.value();  // If f holds an exception, this will throw that exception.

到目前为止,我们还没有做任何 std::future 不能做的事情。但是 future 模式中的一个强有力的方面就是可以做到连锁回调,std::future 目前尚不支持此功能。我们通过方法 Future::then 来表达这个功能:

 
  1. Future<double> f = 

  2.   fooFuture(input)

  3.   .then([](Output o) {

  4.     return o * M_PI;

  5.   })

  6.   .onError([](std::exception const& e) {

  7.     cerr << "Oh bother, " << e.what()

  8.       << ". Returning pi instead." << endl;

  9.     return M_PI;

  10.   });// get() first waits, and then returns the valuecout << "Result: " << f.get() << endl;

在这里我们像使用 onError 一样使用连接起来的 then 去接住可能引发的任何异常。可以将 future 连接起来是一个重要的能力,它允许我们编写串行和并行的计算,并将它们表达在同一个地方,并为之提供明晰的错误处理。

 

串行功能组成

如果你想要按顺序异步计算 a、b、c 和 d,使用传统的回调方式编程就会陷入“回调地狱”- 或者,你使用的语言具备一流的匿名函数(如 C++11),结果可能是“回调金字塔”:

 
  1. // the callback pyramid is syntactically annoying

  2. void asyncA(Output, Callback<OutputA>);

  3. void asyncB(OutputA, Callback<OutputB>);

  4. void asyncC(OutputB, Callback<OutputC>);

  5. void asyncD(OutputC, Callback<OutputD>);

  6. auto result = std::make_shared<double>();

  7. fooAsync(input, [=](Output output) {

  8.   // ...

  9.   asyncA(output, [=](OutputA outputA) {

  10.     // ...

  11.     asyncB(outputA, [=](OutputB outputB) {

  12.       // ...

  13.       asyncC(outputB, [=](OutputC outputC) {

  14.         // ...

  15.         asyncD(outputC, [=](OutputD outputD) {

  16.           *result = outputD * M_PI;

  17.         });

  18.       });

  19.     });

  20.   });

  21. });

  22. // As an exercise for the masochistic reader, express the same thing without

  23. // lambdas. The result is called callback hell.

有了 futures,顺序地使用then组合它们,代码就会变得干净整洁:

 
  1. Future<OutputA> futureA(Output);

  2. Future<OutputB> futureB(OutputA);

  3. Future<OutputC> futureC(OutputB);

  4.  
  5. // then() automatically lifts values (and exceptions) into a Future.

  6.  
  7. OutputD d(OutputC) {

  8.   if (somethingExceptional) throw anException;

  9.   return OutputD();}Future<double> fut =

  10.   fooFuture(input)

  11.   .then(futureA)

  12.   .then(futureB)

  13.   .then(futureC)

  14.   .then(d)

  15.   .then([](OutputD outputD) { // lambdas are ok too

  16.     return outputD * M_PI;

  17.   });

并行功能组成

再回到我们那个 multiFoo 的例子。下面是它在 future 中的样子:

 
  1. using folly::futures::collect;

  2.  
  3. Future<vector<Output>> multiFooFuture(vector<Input> inputs) {

  4.   vector<Future<Output>> futures;

  5.   for (auto input : inputs) {

  6.     futures.push_back(fooFuture(input));

  7.   }

  8.   return collect(futures);}

collect 是一种我们提供的构建块(compositional building block),它以 Future<T> 为输入并返回一个 Future<vector<T>>,这会在所有的 futures 完成后完成。(collect 的实现依赖于-你猜得到-then)有很多其他的构建块,包括:collectAny、collectN、map 和 reduce。

请注意这个代码为什么会看上去与同步版本的 multiFooSync 非常相似,我们不需要担心上下文或线程安全的问题。这些问题都由框架解决,它们对我们而言是透明的。

 

执行上下文

其他一些语言里的 futures 框架提供了一个线程池用于执行回调函数,你除了要知道上下文在另外一个线程中执行,不需要关注任何多余的细节。但是 C++ 的开发者们倾向于编写 C++ 代码,因为他们需要控制底层细节来实现性能优化,Facebook 也不例外。因此我们使用简单的 Executor接口提供了一个灵活的机制来明确控制回调上下文的执行:

 
  1. struct Executor {

  2.   using Func = std::function<void()>;

  3.   virtual void add(Func) = 0;};

你可以向 then 函数传入一个 executor 来命令它的回调会通过 executor 执行。

a(input).then(executor, b);

在这段代码中,b 将会通过 executor 执行,b 可能是一个特定的线程、一个线程池、或是一些更有趣的东西。本方法的一个常见的用例是将 CPU 从 I/O 线程中解放出来,以避免队列中其他请求的排队时间。

 

Futures 意味着你再也不用忘记说对不起

传统的回调代码有一个普遍的问题,即不易对错误或异常情况的调用进行跟踪。程序员在检查错误和采取恰当措施上必须做到严于律己(即使是超人也要这样),更不要说当一场被意外抛出的情况了。Futures 使用包含一个值和一个异常的方式来解决这个问题,这些异常就像你希望的那样与 futures融合在了一起,除非它留在 future 单元里直到被 onErorr 接住,或是被同步地,例如,赋值或取值。这使得我们很难(但不是不可能)跟丢一个应该被接住的错误。

使用 Promise

我们已经大致看过了 futures 的使用方法,下面来说说我们该如何制作它们。如果你需要将一个值传入到 Future,使用 makeFuture:

 
  1. using folly::makeFuture;

  2. std::runtime_error greatScott("Great Scott!");

  3. Future<double> future = makeFuture(1.21e9);

  4. Future<double> future = makeFuture<double>(greatScott);

但如果你要包装一个异步操作,你需要使用 Promise:

 
  1. using folly::Promise;

  2. Promise<double> promise;

  3. Future<double> future = promise.getFuture();

当你准备好为 promise 赋值的时候,使用 setValue、setException 或是 setWith:

 
  1. promise.setValue(1.21e9);

  2. promise.setException(greatScott);

  3. promise.setWith([]{

  4.   if (year == 1955 || year == 1885) throw greatScott;

  5.   return 1.21e9;

  6. });

 

总之,我们通过生成另一个线程,将一个长期运行的同步操作转换为异步操作,如下面代码所示:

 
  1. double getEnergySync(int year) {

  2.   auto reactor = ReactorFactory::getReactor(year);

  3.   if (!reactor) // It must be 1955 or 1885

  4.     throw greatScott;

  5.   return reactor->getGigawatts(1.21);

  6. }

  7. Future<double> getEnergy(int year) {

  8.   auto promise = make_shared<Promise<double>>();

  9.   std::thread([=]{

  10.     promise->setWith(std::bind(getEnergySync, year));

  11.   }).detach();

  12.   

  13.   return promise->getFuture();

  14. }

通常你不需要 promise,即使乍一看这像是你做的。举例来说,如果你的线程池中已经有了一个 executor 或是可以很轻易地获取它,那么这样做会更简单:

Future<double> future = folly::via(executor, std::bind(getEnergySync, year));

用例学习

我们提供了两个案例来解释如何在 Facebook 和 Instagram 中使用 future 来改善延迟、鲁棒性与代码的可读性。

Instagram 使用 futures 将他们推荐服务的基础结构由同步转换为异步,以此改善他们的系统。其结果是尾延迟(tail latency)得以显著下降,并仅用十分之一不到的服务器就实现了相同的吞吐量。他们把这些改动及相关改动带来的益处进行了记录,更多细节可以参考他们的博客。

 

下一个案例是一个真正的服务,它是 Facebook 新闻递送(News Feed)的一个组成部分。这个服务有一个两阶段的叶聚合模式(leaf-aggregate pattern),请求(request)会被分解成多个叶请求将碎片分配到不同的叶服务器,我们在做同样的事情,但根据第一次聚合的结果分配的碎片会变得不同。最终,我们获取两组结果集并将它们简化为一个单一的响应(response)。

下面是相关代码的简化版本:

 
  1. Future<vector<LeafResponse>> fanout(

  2.   const map<Leaf, LeafReq>& leafToReqMap,

  3.   chrono::milliseconds timeout)

  4. {

  5.   vector<Future<LeafResponse>> leafFutures;

  6.   for (const auto& kv : leafToReqMap) {

  7.     const auto& leaf = kv.first;

  8.     const auto& leafReq = kv.second;

  9.     leafFutures.push_back(

  10.       // Get the client for this leaf and do the async RPC

  11.       getClient(leaf)->futureLeafRPC(leafReq)

  12.       // If the request times out, use an empty response and move on.

  13.       .onTimeout(timeout, [=] { return LeafResponse(); })

  14.       // If there's an error (e.g. RPC exception),

  15.       // use an empty response and move on.

  16.       .onError([=](const exception& e) { return LeafResponse(); }));

  17.   }

  18.   // Collect all the individual leaf requests into one Future

  19.   return collect(leafFutures);

  20. }

  21. // Some sharding function; possibly dependent on previous responses.

  22. map<Leaf, LeafReq> buildLeafToReqMap(

  23.     const Request& request,

  24.     const vector<LeafResponse>& responses);

  25. // This function assembles our final response.

  26. Response assembleResponse(

  27.     const Request& request,

  28.     const vector<LeafResponse>& firstFanoutResponses,

  29.     const vector<LeafResponse>& secondFanoutResponses);

  30. Future<Response> twoStageFanout(shared_ptr<Request> request) {

  31.   // Stage 1: first fanout

  32.   return fanout(buildLeafToReqMap(*request, {}),

  33.                 FIRST_FANOUT_TIMEOUT_MS)

  34.   // Stage 2: With the first fanout completed, initiate the second fanout.

  35.   .then([=](vector<LeafResponse>& responses) {

  36.     auto firstFanoutResponses =

  37.       std::make_shared<vector<LeafResponse>>(std::move(responses));

  38.     

  39.     // This time, sharding is dependent on the first fanout.

  40.     return fanout(buildLeafToReqMap(*request, *firstFanoutResponses),

  41.                   SECOND_FANOUT_TIMEOUT_MS)

  42.     

  43.     // Stage 3: Assemble and return the final response.

  44.     .then([=](const vector<LeafResponse>& secondFanoutResponses) {

  45.       return assembleResponse(*request, *firstFanoutResponses, secondFanoutResponses);

  46.     });

  47.   });

  48. }

该服务的历史版本中曾使用只允许整体超时的异步框架,同时使用了传统的“回调地狱”模式。是 Futures 让这个服务自然地表达了异步计算,并使用有粒度的超时以便在某些部分运行过慢时采取更积极的行动。其结果是,服务的平均延迟减少了三分之二,尾延迟减少到原来的十分之一,总体超时错误明显减少。代码变得更加易读和推理,作为结果,代码还变得更易维护。

 

当开发人员拥有了帮助他们更好理解和表达异步操作的工具时,他们可以写出更易于维护的低延迟服务。

结论

Folly Futures 为 C++11 带来了健壮的、强大的、高性能的 futures。我们希望你会喜欢上它(就像我们一样)。如果你想了解更多信息,可以查阅相关文档、文档块以及 GitHub 上的代码。

致谢

Folly Futures 制作团队的成员包括 Hans Fugal,Dave Watson,James Sedgwick,Hannes Roth 和 Blake Mantheny,还有许多其他志同道合的贡献者。我们要感谢 Twitter,特别是 Marius,他在 Facebook 关于 Finagle 和 Futures 的技术讲座,激发了这个项目的创作灵感。


http://chatgpt.dhexx.cn/article/NRSn9mKl.shtml

相关文章

folly::ConcurrentSkipList 详解

SkipList 原理及构造过程 SkipList 是受多层链表的启发而设计出来的。实际上&#xff0c;最理想的情况是上面每一层链表的节点个数&#xff0c;是下面一层的节点个数的一半&#xff0c;这样查找过程就非常类似于一个二分查找&#xff0c;使得查找的时间复杂度可以降低到 O(log…

Facebook Folly源代码分析

Folly是Facebook的一个开源C11组件库&#xff0c;它提供了类似Boost库和STL的功能&#xff0c;包括散列、字符串、向量、内存分配、位处理等&#xff0c;用于满足大规模高性能的需求。 6月初&#xff0c;Facebook宣布将其内部使用的底层C组件库Folly开源&#xff0c;本文尝试对…

folly官方例子

folly官方例子 Future<vector<LeafResponse>> fanout(const map<Leaf, LeafReq> &leafToReqMap,chrono::milliseconds timeout) {vector<Future<LeafResponse>> leafFutures;for (const auto &kv : leafToReqMap) {const auto &leaf…

Facebook 的 C++ 11 组件库 Folly Futures

英文原版&#xff1a;https://code.facebook.com/posts/1661982097368498/futures-for-c-11-at-facebook/ https://www.oschina.net/translate/futures-for-c-11-at-facebook http://www.lupaworld.com/article-254822-1.html Futures 是一种通过自然的、可组合的方式表达异…

交叉编译folly库

假定交叉编译链工具所在目录为&#xff1a;/home/softwares/gcc-ubuntu-9.3.0-2020.03-x86_64-aarch64-linux-gnu/bin/&#xff0c;其c编译器为&#xff1a;/home/softwares/gcc-ubuntu-9.3.0-2020.03-x86_64-aarch64-linux-gnu/bin/aarch64-linux-gnu-g 1. 下载folly源码&…

folly库安装(5)folly的安装

上面这些准备工作做完了&#xff0c;现在就可以安装folly了&#xff0c;其实这时folly的安装已经非常顺利了。网上有人说folly的安装很麻烦&#xff0c;最重要是上面的准备工作没做好&#xff0c;只要你按照我上面的文章&#xff0c;一步步做下来&#xff0c;安装成功是没问题的…

揭秘Facebook官方底层C++函数Folly

2019独角兽企业重金招聘Python工程师标准>>> Folly与Boost、当然还有std等组件库的关系是互为补充&#xff0c;而不是彼此竞争。实际上&#xff0c;只有当我们需要的东西既没有&#xff0c;也无法满足所需的性能要求时&#xff0c;我们才开始定义自己的组件。 性能问…

《设计原则》(一)

易理解性和易使用性的设计原则 提供一个好的概念模式&#xff1b;&#xff08;一个好的概念模式使用户能够预测操作的行为效果&#xff09;可视性(消除执行阶段和评估阶段的鸿沟)&#xff1b;自然匹配&#xff1b;&#xff08;利用物理环境类比和文化标准概念、空间类比&#…

C++设计模式的设计原则(面向对象八大设计原则)

面向对象设计八大设计原则 一、温故面向对象二、八大设计原则三、以史为鉴 先掌握八大设计原则&#xff0c;再详细看23种设计模式&#xff08;&#x1f448;点我&#xff09; 一、温故面向对象 &#xff08;1&#xff09;隔离变化&#xff1a;从宏观层面上来看&#xff0c;面向…

设计原则设计模式

导论 什么是设计原则&#xff1a;判断程序设计质量好坏的准则。什么是设计模式&#xff1a;软件设计过程中重复出现问题的解决方案设计原则的作用&#xff1a;指导抽象、类、类关系设计&#xff0c;相当于指导设计程序基础框架&#xff08;Rank-分层、Role-角色、Relation-类关…

设计原则详解

1.单一职责 一个类&#xff0c;只有一个引起它变化的原因。应该只有一个职责。每一个职责都是变化的一个轴线&#…

五大设计原则——SOLID

目录 简介&#xff1a; 1、单一职责原则&#xff08;SRP&#xff09; 2、开闭原则&#xff08;OCP&#xff09; 3、里式替换原则&#xff08;LSP&#xff09; 4、依赖倒置原则 (DIP) 5、接口隔离原则 (ISP) 简介&#xff1a; 无论是软件系统设计&#xff0c;还是代码实现…

1. 设计原则

文章目录 设计原则思维导图核心理论SOLID单一职责开放封闭里式替换接口隔离依赖反转 KISSDRYLOD 设计原则思维导图 核心理论 基于接口编程 “基于接口而非实现编程” - “Program to an interface, not an implementation”。 “接口”就是一组“协议”或者“约定”&#xff…

七大设计原则

一、七大设计原则 &#xff08;1&#xff09;单一职责原则 &#xff08;2&#xff09;接口隔离原则 &#xff08;3&#xff09;依赖倒置原则 &#xff08;4&#xff09;里氏替换原则 &#xff08;5&#xff09;开闭原则 &#xff08;6&#xff09;迪米特法则 &#xff0…

chrome浏览器截长图

使用chrome浏览器 打开开发者模式(更多工具->开发者工具) mac 按commandshiftp windows 按ctrlshiftp 然后输入capture 选择capture full size screenshot就可以了 截了个长图的例子

手把手教你截长图

1.截长图的工具 相信很多小伙伴在平时工作做都会碰见截图的问题&#xff0c;那正常的图&#xff0c;我们有各种方式去截取&#xff0c;例如&#xff1a;QQ的CtrlAltA&#xff0c;微信的AltA等等 但是呢&#xff0c;如果要用到长图的时候&#xff0c;就束手无策了&#xff0c;这…

python如何截长图_利用 Python + Selenium 实现对页面的指定元素截图(可截长图元素)...

对WebElement截图 WebDriver.Chrome自带的方法只能对当前窗口截屏&#xff0c;且不能指定特定元素。若是需要截取特定元素或是窗口超过了一屏&#xff0c;就只能另辟蹊径了。 WebDriver.PhantomJS自带的方法支持对整个网页截屏。 下面提供几种思路。 方式一 针对WebDriver.Chro…

谷歌浏览器怎么截长图?

我们在使用电脑浏览网页的时候难免会需要进行一些长图的截取&#xff0c;而一般的截图只能实现一部分截取&#xff0c;那么我们要如何去实现这个操作呢&#xff1f;下面小编就给大家介绍一下怎么在谷歌浏览器上截长图的操作。 谷歌浏览器网页截长图怎么截&#xff1f; 1、进入C…

html2canvas截长图

github链接 一、下载运行后选择下图的html2canvas即可直接去到路由界面测试 二、下图是html2canvas路由页面&#xff0c;点击右上角的生成图片即可下载长图 三、源码路径&#xff08;html2canvas源码github&#xff09; 四、源码&#xff08;关键在generateImage 这个方法&…

selenium+phantomjs截长图踩坑

目录 需求背景&#xff1a; 调研 phantomjs selenium 服务器部署 需求背景 BI上的报表需要设置定时任务截图发邮件到订阅人的邮箱中。刚开始以为截图的活是前端的&#xff0c;后来发现使自己的锅。 调研 截图的研究了一下&#xff0c;主流应该是 selenium 和 phantomjs。…