FoundationDB 的 Flow——将基于 Actor 的并发引入 C++11
摘要
Flow 是 C++11 的编程语言扩展,带来了基于 Actor 的并发,支持使用 future 和 promise 进行高效异步通信,并支持确定性模拟以进行可靠性测试。
暂无内容
查看缓存全文
缓存时间: 2026/07/03 02:10
# Flow — FoundationDB ON 文档 来源:https://apple.github.io/foundationdb/flow.html
## 工程挑战
FoundationDB 在创立之初就设定了雄心勃勃的目标:既要单节点的高性能(https://apple.github.io/foundationdb/performance.html),又要可扩展性(https://apple.github.io/foundationdb/scalability.html)。我们深知,要实现这些目标,在开发 FoundationDB 核心时将面临严峻的工程挑战。我们需要实现类似 Erlang(http://en.wikipedia.org/wiki/Erlang_(programming_language))或 .NET 中的 Async 库(http://msdn.microsoft.com/en-us/library/vstudio/hh191443.aspx)所支持的高效异步通信进程,但同时还需要 C++ 的原始速度和 I/O 效率。最后,我们还需要进行大规模仿真,以确保系统在大型集群上的可靠性和容错能力。
为了应对这些挑战,我们开发了几种新工具,其中第一个就是 Flow——一种将基于角色的并发(http://en.wikipedia.org/wiki/Actor_model)引入 C++11 的新编程语言。为了增加这种能力,Flow 引入了许多新的关键字和控制流原语来管理并发。Flow 实现为一个编译器,它分析异步函数(actor)并将其重写为一个包含多个子函数的对象,这些子函数使用回调来避免阻塞(参见 streamlinejs(https://github.com/Sage/streamlinejs),它用 JavaScript 实现了类似的概念)。Flow 编译器的输出是普通的 C++11 代码,然后使用传统工具编译成二进制文件。Flow 还为我们的仿真工具提供输入,该工具对整个系统(包括其物理接口和故障模式)进行确定性仿真。
简而言之,Flow 允许在 C++ 中以可维护且可扩展的方式实现高效并发,实现了三个主要工程目标:
- 高性能(通过编译为本地代码),
- 基于角色的并发(用于高生产率开发),
- 仿真支持(用于测试)。
## 初探
Flow 中的角色通过一种名为 future 的数据类型相互异步传递消息。当一个角色需要某个数据值才能继续计算时,它会等待该值,而不会阻塞其他角色。下面这个简单的角色执行异步加法。它接收一个 future 整数和一个普通整数作为偏移量,等待 future 整数,然后返回该值与偏移量之和:
```cpp
ACTOR Future<int> asyncAdd(Future<int> f, int offset) {
int value = wait( f );
return value + offset;
}
```
## Flow 特性
Flow 的新关键字和控制流原语支持在组件之间异步传递消息。以下是简要概述。
### Promise 和 Future
连接异步发送方和接收方的数据类型是 `Promise<T>` 和 `Future<T>`,其中 `T` 是某种 C++ 类型。当发送方持有 `Promise<T>` 时,它代表一个承诺,即在未来某个时刻将类型 `T` 的值传递给持有 `Future<T>` 的一方。相反,持有 `Future<T>` 的接收方可以异步继续计算,直到真正需要该 `T` 值为止。
Promise 和 future 可以在单个进程内使用,但它们在分布式系统中的真正优势在于可以穿越网络。例如,一台计算机可以创建一个 promise/future 对,然后将 promise 通过网络发送给另一台计算机。promise 和 future 仍将保持连接,当远程计算机履行 promise 时,原始 future 的持有者将看到该值出现。
### wait()
当持有 `Future<T>` 的接收方需要 `T` 值来继续计算时,它会调用 `wait()` 语句,并将 `Future` 作为参数。`wait()` 语句允许调用角色暂停执行,直到 future 的值被设置,然后返回类型 `T` 的值。在等待期间,其他角色可以继续执行,从而在单个进程内实现异步并发。
### ACTOR
只有标记了 `ACTOR` 标签的函数才能调用 `wait()`。角色是异步工作的基本单元,可以组合起来创建复杂的消息传递系统。通过组合角色,可以将 future 链接起来,使得一个角色的结果依赖于另一个角色的输出。角色声明为返回 `Future<T>`,如果角色的返回值仅用于信号传递,则 `T` 可以是 `Void`。每个角色都会被预处理成一个 C++11 类,其中包含内部回调和辅助函数。
### State
`state` 关键字用于限定变量的作用域,使其在角色中的多个 `wait()` 语句之间可见。下面示例角色中说明了 `state` 变量的用法。
### PromiseStream、FutureStream
当一个组件需要处理异步消息的*流*而不是单个消息时,可以使用 `PromiseStream` 和 `FutureStream`。这些构造支持两个重要特性:多路复用和可靠的消息传递。它们在 Flow 设计模式中也扮演着重要角色。例如,FoundationDB 中的许多服务器将其接口暴露为一个 promise 流结构体,每种请求类型对应一个。
### waitNext()
`waitNext()` 是 `wait()` 对流(stream)的对应物。它会暂停程序执行,等待 `FutureStream` 中的下一个值。如果流中已经有值准备就绪,则执行会立即继续。
### choose ... when
`choose` 和 `when` 构造允许角色以有序且可预测的方式同时等待多个 future。
### 示例:服务器接口
以下是一个运行在单台服务器上、通过网络通信的角色。其功能是根据来自其他角色的异步消息维护一个计数器。它实现了一个接口,该接口包含一个循环,循环内有一个 `choose` 语句,每种请求类型对应一个 `when`。每个 `when` 使用 `waitNext()` 异步等待流中的下一个请求。add 和 subtract 接口修改计数器本身,计数器通过 state 变量存储。get 接口接收一个 `Promise<int>` 而不是简单的 `int`,以便于发送返回消息。
如果直接使用 C++ 编写等效代码,开发人员必须实现一组复杂的带有异常处理的回调,这需要更多的工程工作。Flow 使实现这种异步协调变得容易得多,而且性能毫不损失:
```cpp
ACTOR void serveCountingServerInterface( CountingServerInterface csi) {
state int count = 0;
while (1) {
choose {
when (int x = waitNext(csi.addCount.getFuture())){
count += x;
}
when (int x = waitNext(csi.subtractCount.getFuture())){
count -= x;
}
when (Promise<int> r = waitNext(csi.getCount.getFuture())){
r.send( count ); // goes to client
}
}
}
}
```
## 注意事项
尽管 Flow 代码看起来很像 C++,但它并不是。它有不同的规则,并且文件需要预处理。在编写 Flow 代码时,始终要牢记这一点。我们仍然希望能够使用 IDE 和现代编辑器(如带有 cquery 等语言服务器或基于 clang 的补全引擎如 ycm)。因此,在 flow 中有一个头文件 `actorcompiler.h`,它定义了一些预处理器定义,使 Flow 能够作为普通 C++ 代码编译。CMake 甚至支持一种特殊模式,从而不需要预处理 flow 文件。可以通过向 cmake 传递 `-DOPEN_FOR_IDE=ON` 来使用该模式。此外,我们会在源代码目录中生成一个特殊的 `compile_commands.json`,以支持在查找编译数据库的 IDE 和编辑器中打开项目。
然而,某些预处理器定义并不能解决所有问题。在编写 Flow 代码时,程序员需要注意以下几点:
- 局部变量在调用 `wait` 后不会保留。因此,以下代码在 Flow 中是合法的,但在 C++ 中是非法的:
```cpp
ACTOR void foo {
int i = 0;
wait(someFuture);
int i = 2;
wait(someOtherFuture)
}
```
为了不破坏 IDE 支持,可以重命名第二个变量,或者,如果不想这样做(因为可能会使代码难以阅读),可以使用作用域:
```cpp
ACTOR void foo {
{
int i = 0;
wait(someFuture);
}
{
int i = 2;
wait(someOtherFuture)
}
}
```
- 一个 `ACTOR` 在内部会被编译成一个类。这意味着在 actor 函数内部,`this` 是一个指向该类的有效指针。但显式使用它们(或如后所述隐式使用)会破坏 IDE 支持。可以使用 `THIS` 和 `THIS_ADDR` 代替。但请注意,在 IDE 模式下,`THIS` 的类型是 `nullptr_t`,而在正常编译模式下,它是 actor 类型。
- Lambda 表达式和 state 变量在某种意义上有些奇怪。在 actor 编译后,state 变量成为编译后 actor 类的成员变量。在 IDE 模式下,它被视为普通的局部变量。这可能会导致一些令人惊讶的副作用。因此,以下代码只有在 `Foo::bar` 被定义为 `const` 时才能编译:
```cpp
ACTOR foo() {
state Foo f;
foo([=]() {
f.bar();
})
}
```
如果不是,则必须将成员作为引用显式传递:
```cpp
ACTOR foo() {
state Foo f;
auto x = &f;
foo([x]() {
x->bar();
})
}
```
- Flow 中的 state 变量不遵循普通的作用域规则。因此,在 Flow 中,state 变量可以在内层作用域中定义,然后在外层作用域中使用。为了不在 IDE 模式下破坏编译,请始终将 state 变量定义在其将被使用的最外层作用域中。
相似文章
bytedance/deer-flow
DeerFlow 2.0 是 ByteDance 开源的一个超级代理框架,它通过可扩展的技能编排子代理、记忆和沙盒。它于2026年2月28日取得了 GitHub Trending 第一名。
FlowBank: 通过预计算和重用的查询自适应智能体工作流优化
FlowBank引入了一个三阶段框架,通过预计算一组多样化的可重用工作流并自适应地为每个查询选择最佳工作流,来优化LLM多智能体系统中的智能体工作流,在保持成本竞争力的同时实现了更高的分数。
通过延续传递数据库
本文解释了如何利用延续传递风格(CPS)来融合数据库操作符,避免中间结果物化并提升性能,作为向量化和编译的替代方案。作者通过一个数值列表操作的简化示例进行说明,并描述了其灵感来源于一篇短文。
DataFlow:面向数据为中心AI时代的统一数据准备与工作流自动化的LLM驱动框架
DataFlow是一个LLM驱动的框架,用于自动化数据准备和工作流工程,具备近200个可复用算子和六个领域通用流程,可在数学、代码和Text-to-SQL等任务上提升LLM性能。
@flowstated: fable 5 composer 2.5 fast
Fable 5 与 Composer 2.5 Fast 集成,以增强 AI 代码生成能力。