【译】Async/Await(三)——Aysnc/Await模式

原文标题:Async/Await
原文连接:https://os.phil-opp.com/async-await/#multitasking
公众号: Rust 碎碎念
翻译 by: Prayinghtml

Async/Await 模式(The Async/Await Pattern)

async/await 背后的思想是让程序员可以像写普通的同步代码那样来编写代码,由编译器负责将其转为异步代码。它基于asyncawait两个关键字来发挥做用。async关键字能够被用于一个函数签名,负责把一个同步函数转为一个返回 future 的异步函数。程序员

async fn foo() -> u32 {
    0
}

// the above is roughly translated by the compiler to:
fn foo() -> impl Future<Output = u32> {
    future::ready(0)
}

这个关键字是没法单独发挥做用的,可是在async函数内部,await关键字能够被用于取回(retrieve)一个 future 的异步值。web

async fn example(min_len: usize) -> String {
    let content = async_read_file("foo.txt").await;
    if content.len() < min_len {
        content + &async_read_file("bar.txt").await
    } else {
        content
    }
}

尝试在 playground 上运行这段代码[1]闭包

这个函数是对example函数的一个直接转换,example函数使用了上面提到的组合子函数(译注:在译文 Async/Await(二)中)。经过使用.await操做,咱们可以在不须要任何闭包或者Either的状况下检索一个 future 的值。所以,咱们能够像写普通的同步代码同样来写咱们的代码,不一样之处在于咱们写的仍然是异步代码。异步

状态机转换

编译器在背后把async函数体转为一个状态机(state machine)[2],每个.await调用表示一个不一样的状态。对于上面的example函数,编译器建立了一个带有下面四种状态的状态机:async

每一个状态表示函数中一个不一样的暂停点。"Start"和"End"状态表示开始执行的函数和执行结束的函数。"Waiting on foo.txt"状态表示函数当前正在等待第一个async_read_file的结果。相似地,"Waiting on bar.txt"表示函数正在等待第二个async_read_file结果。编辑器

这个状态机经过让每个poll调用成为一次状态转换来实现Future trait。函数

上面这张图用箭头表示状态切换,用菱形表示分支路径。例如,若是foo.txt没有准备好,就会选择标记"no"的路径而后进入”Waiting on foo.txt“状态。不然,就会选择"yes"路径。中间较小的没有标题的红色菱形表示example函数的if content.len() < 100分支。oop

咱们能够看到第一个poll调用启动了这个函数并使函数一直运行直到它到达一个还没有就绪的 future。若是这条路径上的全部 future 都已就绪,该函数就能够一直运行到"End"状态,这里它把本身的结果包装在Poll::Ready中而后返回。不然,状态机进入到一个等待状态并返回"Poll::Pending"。在下一个poll调用时,状态机从上次等待状态开始而后重试上次操做。布局

保存状态

为了可以从上次等待状态继续下去,状态机必须在内部记录当前状态。此外,它还必需要保存下次poll调用时继续执行须要的全部变量。这也正是编译器大展身手的地方:由于编译器知道哪一个变量在什么时候被使用,因此它能够自动生成结构体,这些结构体准确地包含了所须要的变量。

例如,编译器能够针对上面的example函数生成相似下面的结构体:

//  再次放上`example` 函数 ,你就不用去上面找它了
async fn example(min_len: usize) -> String {
    let content = async_read_file("foo.txt").await;
    if content.len() < min_len {
        content + &async_read_file("bar.txt").await
    } else {
        content
    }
}

// 编译器生成的状态结构体:

struct StartState {
    min_len: usize,
}

struct WaitingOnFooTxtState {
    min_len: usize,
    foo_txt_future: impl Future<Output = String>,
}

struct WaitingOnBarTxtState {
    content: String,
    bar_txt_future: impl Future<Output = String>,
}

struct EndState {}

在"Start"和"Waiting on foo.txt"这两个状态(分别对应 StartState 和 WaitingOnFooTxtState 结构体)里,参数min_len须要被存储起来,由于在后面和content.len()进行比较时会须要用到它。"Waiting on foo.txt"状态还须要额外存储一个foo_txt_future,它表示由async_read_file调用返回的 future。这个 future 在当状态机继续的时候会被再次轮询(poll),因此它也须要被保存起来。

"Waiting on bar.txt"状态(译注:对应WaitingOnBarTxtState 结构体)包含了content变量,由于它会在bar.txt就绪后被用于字符串拼接。该状态还存储了一个bar_txt_future用以表示对bar.txt正在进行的加载。WaitingOnBarTxtState结构体不包含min_len变量由于它在和 content.len()比较后就再也不被须要了。在"End"状态下,没有存储任何变量,由于函数在这里已经运行完成。

注意,这里只是编译器针对代码可能生成的一个示例。结构体的命名以及字段的布局都是实现细节而且可能有所不一样。

完整的状态机类型

虽然具体的编译器生成代码是一个实现细节,可是它有助于咱们理解example函数生成的状态机看起来是怎么样的?咱们已经定义了表示不一样状态的结构体而且包含须要的字段。为了可以在此基础上建立一个状态机,咱们能够把它组合进enum

enum ExampleStateMachine {
    Start(StartState),
    WaitingOnFooTxt(WaitingOnFooTxtState),
    WaitingOnBarTxt(WaitingOnBarTxtState),
    End(EndState),
}

咱们为每一个状态定义一个单独的枚举变量,而且把对应的状态结构体添加到每一个变量中做为一个字段。为了实现状态转换,编译器基于example函数生成了一个Future trait 的实现:

impl Future for ExampleStateMachine {
    type Output = String// return type of `example`

    fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
        loop {
            match self { // TODO: handle pinning
                ExampleStateMachine::Start(state) => {…}
                ExampleStateMachine::WaitingOnFooTxt(state) => {…}
                ExampleStateMachine::WaitingOnBarTxt(state) => {…}
                ExampleStateMachine::End(state) => {…}
            }
        }
    }
}

future 的Output类型是String,由于它是example函数的返回类型。为了实现poll函数,咱们在loop内部对当前的状态使用一个 match 语句。其思想在于只要有可能就切换到下一个状态,当没法继续的时候就使用一个显式的return Poll::Pending

简单起见,咱们只能展现简化的代码且不对pinning[3]、全部权、生命周期等进行处理。因此,这段代码以及接下来的代码就当成是伪代码,不要直接使用。固然,实际上编译器生成的代码已经正确地处理好了一切,尽管多是以另外一种方式。

为了让代码片断尽量地小,咱们为每一个 match 分支单独展现代码。让咱们先从Start状态开始:

ExampleStateMachine::Start(state) => {
    // from body of `example`
    let foo_txt_future = async_read_file("foo.txt");
    // `.await` operation
    let state = WaitingOnFooTxtState {
        min_len: state.min_len,
        foo_txt_future,
    };
    *self = ExampleStateMachine::WaitingOnFooTxt(state);
}

状态机在函数开始时就处于Start状态,在这种状况下,咱们从example函数体执行全部的代码,直至遇到第一个.await。为了处理.await操做,咱们把self状态机的状态更改成WaitingOnFooTxt,该状态包括了对WaitingOnFooTxtState的构造。

由于match self {...} 状态是在一个循环里执行的,这个执行接下来跳转到WaitingOnFooTxt分支:

ExampleStateMachine::WaitingOnFooTxt(state) => {
    match state.foo_txt_future.poll(cx) {
        Poll::Pending => return Poll::Pending,
        Poll::Ready(content) => {
            // from body of `example`
            if content.len() < state.min_len {
                let bar_txt_future = async_read_file("bar.txt");
                // `.await` operation
                let state = WaitingOnBarTxtState {
                    content,
                    bar_txt_future,
                };
                *self = ExampleStateMachine::WaitingOnBarTxt(state);
            } else {
                *self = ExampleStateMachine::End(EndState));
                return Poll::Ready(content);
            }
        }
    }
}

在这个 match 分支,咱们首先调用foo_txt_futurepoll函数。若是它还没有就绪,咱们就退出循环而后返回Poll::Pending。由于这种状况下self仍处于WaitingOnFooTxt状态,下一次的poll调用将会进入到相同的 match 分支而后重试对foo_txt_future轮询。

foo_txt_future就绪后,咱们把结果赋予content变量而且继续执行example函数的代码:若是content.len()小于保存在状态结构体里的min_lenbar.txt文件会被异步地读取。咱们再次把.await操做转换为一个状态改变,此次改变为WaitingOnBarTxt状态。由于咱们在一个循环里面正在执行match,执行流程直接跳转到新的状态对应的 match 分支,这个新分支对bar_txt_future进行了轮询。

一旦咱们进入到else分支,后面就再也不会进行.await操做。咱们到达了函数结尾并返回包装在Poll::Ready中的content。咱们还把当前的状态改成了End状态。

WaitingOnBarTxt状态的代码看起来像下面这样:

ExampleStateMachine::WaitingOnBarTxt(state) => {
    match state.bar_txt_future.poll(cx) {
        Poll::Pending => return Poll::Pending,
        Poll::Ready(bar_txt) => {
            *self = ExampleStateMachine::End(EndState));
            // from body of `example`
            return Poll::Ready(state.content + &bar_txt);
        }
    }
}

WaitingOnFooTxt状态相似,咱们从轮询bar_txt_future开始。若是它仍然是 pending,咱们退出循环而后返回Poll::Pending。不然,咱们能够执行example函数最后的操做:未来自 future 的结果与content相链接。咱们把状态机更新到End状态,而后将结果包装在Poll::Ready中进行返回。

最后,End状态的代码看起来像下面这样:

ExampleStateMachine::End(_) => {
    panic!("poll called after Poll::Ready was returned");
}

在返回Poll::Ready以后,future 不该该被再次轮询。所以,当咱们已经处于End状态时,若是poll被调用咱们将会 panic。

咱们如今知道编译器生成的状态机以及它对Future trait 的实现是什么样子的了。实际上,编译器是以一种不一样的方式来生成代码。(若是你感兴趣的话,当前的实现是基于生成器(generator)[4]的,可是这只是一个实现细节)。

最后一部分是生成的示例函数自己的代码。记住,函数签名是这样定义的:

async fn example(min_len: usize) -> String

由于完整的函数体实现是经过状态机来实现的,这个函数惟一须要作的事情是初始化状态机并将其返回。生成的代码看起来像下面这样:

fn example(min_len: usize) -> ExampleStateMachine {
    ExampleStateMachine::Start(StartState {
        min_len,
    })
}

这个函数再也不有async修饰符,由于它如今显式地返回一个ExampleStateMachine类型,这个类型实现了Future trait。正如所指望的,状态机在Start状态被构造,并使用min_len参数初始化与之对应的状态结构体。

记住,这个函数没有开始状态机的执行。这是 Rust 中 future 的一个基本设计决定:在第一次轮询以前,它们什么都不作。

参考资料

[1]

尝试在 playground 上运行这段代码: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=d93c28509a1c67661f31ff820281d434

[2]

状态机(state machine): https://en.wikipedia.org/wiki/Finite-state_machine

[3]

pinning: https://doc.rust-lang.org/stable/core/pin/index.html

[4]

生成器(generator): https://doc.rust-lang.org/nightly/unstable-book/language-features/generators.html

相关文章
相关标签/搜索