foundationdb代码阅读--Flow语言
Flow语言
Foundationdb一开始就有一个远大的目标,即每个节点的高性能和可扩展性。实际工程实现时遇到很大的挑战,即需要实现Elang和.NET的异步通信框架,但仍保留C++原始的高性能和IO效率。
Foundationdb为了实现这个目标,开发了一种新的编程语言,Flow语言,将actor-based concurrency引入到C++11中,并引入了一些新的关键字和控制流原语来管理并发。Flow实现了一个编译器,通过分析异步函数(actor)并重写为一个对象,其中包含很多不同的子函数,这些子函数使用回调来避免阻塞。Flow编译器的输出是标准的C++11代码,可以使用传统工具编译成二进制文件,通过这些实现了三个主要的工程目标:
1、通过编译原生代码,提升性能。
2、actor-based concurrency实现了搞生产率开发
3、仿真支持,用于支持测试
看个简单的例子
Flow中的Actor使用称为future的数据类型来相互接收异步消息。当actor需要数据继续计算时,它在不阻塞其他actor的情况下等待数据。actor执行异步加法的示例如下
ACTOR Future<int> asyncAdd(Future<int> f, int offset)
{
int value = wait(f);//等待f
return value + offset;返回f和offset的和
}
Flow语言的特性
Flow的新关键字和控制流原语支持在组件之间异步传递消息,简单描述如下。
Promise<T>和Future<T>用来连接异步发送者和接收者,其中T表示基本的C++类型。持有Promise<T>的发送者将来会给持有Future<T>的接收者发送类型为T的数据。相反,持有Future<T>的接收者可以异步连续计算,直到等待新的T类型的数据到来。
Promise和Future可以用在单个进程中,它真正的优势是在分布式系统中可以穿越网络。例如,一台机器可以创建Promise/Future对,然后通过网络将Promise发给另一台机器。Promise和Future一直保持连接状态,当远端机器填充Promise后,持有Future的机器就能收到。
wait()
只有标有ACTOR标记的函数才能调用wait(),Actors是异步工作的基本单元,可以组合起来创建复杂的消息传递系统。通过组合Actor,Futures可以链接在一起,这样一方的结果取决于另一方的输出。
ACTOR
当持有Future<T>的接收方需要类型T的数据继续计算时,它调用wait()语句,并将Future<T>作为参数。Wait()语句允许调用者暂停执行,直到收到数据,在等待期间,其他actor可以继续执行,实现了单个进程内的异步并发性。
Actor被声明为返回Future<T>,如果返回值是void类型,则仅用于发送信号。每个actor被预处理成一个C++11的类。
State
State关键字用于限定变量的作用域,以便它在一个actor中的多个wait()语句中可见。
PromiseStream<T>, FutureStream<T>
当组件希望处理异步消息流而不是单个消息时,它可以使用PromiseStream<T>和FutureStream<T>,该结构有两个重要特性:
多路复用和消息的可靠传递。比如Foundationdb的很多服务将接口暴露为PromiseStream,每个请求对应一个类型。
waitNext()
与wait()对应。暂停程序执行并等待FutureStream()中的值。一旦stream中有数据,将立刻执行。
choose . . . when
允许actor以有序和可预测的方式等待多个Futures。
下面看一个服务接口的示例
功能是维护一个计数,以响应来自其他actor的异步消息。
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++有很多相似的地方,但它毕竟不是C++,在编程时一定要注意这一点。
我们仍然希望使用IDE和 现代编辑器。因此有一个头文件actorcompiler.h用于存放Flow语言的宏定义,使得Flow语言编译起来像普通的C++代码。CMake甚至支持一种特殊的模式,它不会处理Flow文件。此模式通过将-DOPEN_FOR_IDE=ON传给cmake来使用。此外,我们还生成了一个特殊的compile_commands.json到源码目录,该目录支持在查找编译数据库的IDE和编辑器中打开项目。
不过,某些预处理定义并不能解决所有问题。在编程时,必须注意以下事项:
局部变量在调用完wait后销毁。虽然这是合法的Flow代码,但并不是合法的C++代码:
ACTOR void foo {
int i = 0;
wait(someFuture);
int i = 2;
wait(someOtherFuture)
}
可以通过增加作用域的方式来使代码成为合法的C++代码,如下所示:
ACTOR void foo {
{
int i = 0;
wait(someFuture);
}
{
int i = 2;
wait(someOtherFuture)
}
}
ACTOR在内部被编译成类。这意味着在actor函数中,这是指向该类的有效指针,
但是显式使用它们是IDE不支持的,可以用THIS和THIS_ADDR来代替。但要小心,
因为这在IDE模式下是nullptr_t类型,在正常编译下是actor类型。
Lambda函数和状态变量在某种情况下看起来很奇怪。在actor编译后,状态变量是已编译actor类的成员。在IDE模式下,被视为
正常的局部变量,这可能会有一些意想不到的副作用,因此,只有当方法FOO::bar被定义为const时,以下代码才能编译通过:
ACTOR foo() { state Foo f; foo([=]() { f.bar(); }) }
如果不是,则必须将该成员作为引用显式传递:
ACTOR foo() { state Foo f; auto x = &f; foo([x]() { x->bar(); }) }
Flow中的状态变量并不遵循正常的作用域规则。因此在Flow中,状态变量可以在内部范围中定义,然后可以在外部范围中使用。
为了能在IDE模式中编译通过,需要注意始终在将要使用的最外层范围内定义状态变量。