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模式中编译通过,需要注意始终在将要使用的最外层范围内定义状态变量。
 
posted @ 2021-12-26 22:10  飞舞的小蛇  阅读(496)  评论(0编辑  收藏  举报