Tokio 实验性 async / await 支持

2018年8月27日

星期一快乐!

如果您还没听说,async / await 是 Rust 正在开发的一个重要新特性。它的目标是使异步编程变得容易(好吧,至少比今天稍微容易一点)。这项工作已经进行了一段时间,并且今天已经在 Rust nightly 频道上可用。

我很高兴地宣布 Tokio 现在有了实验性的 async / await 支持!让我们深入了解一下。

开始入门

首先,Tokio async/await 支持由一个新的 crate 提供,这个 crate 的名字很有创意,叫做 tokio-async-await。这个 crate 是 tokio 的一个垫片层。它包含与 tokio 相同的所有类型和函数(作为重新导出),以及用于 async / await 的其他辅助工具。

要使用 tokio-async-await,您需要从配置为使用 Rust 2018 edition 的 crate 中依赖它。它也只适用于最近的 Rust nightly 版本。

在您的应用程序的 Cargo.toml 中,添加以下内容

# At the very top of the file
cargo-features = ["edition"]

# In the `[packages]` section
edition = "2018"

# In the `[dependencies]` section
tokio = {version = "0.1", features = ["async-await-preview"]}

然后,在您的应用程序中,执行以下操作

// The nightly features that are commonly needed with async / await
#![feature(await_macro, async_await, futures_api)]

// This pulls in the `tokio-async-await` crate. While Rust 2018
// doesn't require `extern crate`, we need to pull in the macros.
#[macro_use]
extern crate tokio;

fn main() {
    // And we are async...
    tokio::run_async(async {
        println!("Hello");
    });
}

并运行它(使用 nightly 版本)

cargo +nightly run

您就正在使用 Tokio + async / await 了!

请注意,要 spawn async 代码块,应使用 tokio::run_async 函数(而不是 tokio::run)。

深入了解

现在,让我们构建一些简单的东西:一个 echo 服务器 (耶!)。

// Somewhere towards the top

#[macro_use]
extern crate tokio;

use tokio::net::{TcpListener, TcpStream};
use tokio::prelude::*;

// more to come...

// The main function
fn main() {
  let addr: SocketAddr = "127.0.0.1:8080".parse().unwrap();
  let listener = TcpListener::bind(&addr).unwrap();

    tokio::run_async(async {
        let mut incoming = listener.incoming();

        while let Some(stream) = await!(incoming.next()) {
            let stream = stream.unwrap();
            handle(stream);
        }
    });
}

在这个例子中,incoming 是已接受的 TcpStream 值的 stream。我们正在使用 async / await 来迭代 stream。目前,只有等待单个值(future)的语法,所以我们使用 next 组合器来获取 stream 中下一个值的 future。这使我们可以使用 while 语法迭代 stream。

一旦我们获得 stream,它将被传递给 handle 函数进行处理。让我们看看它是如何实现的。

fn handle(mut stream: TcpStream) {
    tokio::spawn_async(async move {
        let mut buf = [0; 1024];

        loop {
            match await!(stream.read_async(&mut buf)).unwrap() {
                0 => break, // Socket closed
                n => {
                    // Send the data back
                    await!(stream.write_all_async(&buf[0..n])).unwrap();
                }
            }
        }
    });
}

就像 run_async 一样,有一个 spawn_async 函数可以将 async 代码块 spawn 为 tasks。

然后,为了执行 echo 逻辑,我们从 socket 读取数据到 buffer 中,并将数据写回同一个 socket。因为我们正在使用 async / await,我们可以使用一个看起来是栈分配的数组(它实际上最终会在堆上)。

请注意,TcpStream 具有 read_asyncwrite_all_async 函数。这些函数执行与 stdReadWrite traits 上存在的同步等效函数相同的逻辑。不同之处在于,它们返回可以被 await 的 futures。

*_async 函数在 tokio-async-await crate 中通过使用 extension traits 定义。这些 traits 通过 use tokio::prelude::*; 行导入。

这只是一个开始,请查看仓库中的 examples 目录以获取更多信息。甚至有一个使用了 hyper 的示例。

一些注意事项

首先,tokio-async-await crate 仅提供对 async / await 语法的兼容性。它提供对 futures 0.3 crate 的支持。预计用户将继续使用 futures 0.1 以保持与 Tokio 的兼容性。

为了使这项工作正常进行,tokio-async-await crate 定义了自己的 await! macro。这个 macro 是 std 提供的 macro 的一个垫片层,它能够等待 futures 0.1 futures。这就是兼容层能够保持轻量级无样板代码的原因。

这只是一个开始。async / await 支持将随着时间的推移继续发展和改进,但这足以让每个人开始使用!

就这样,祝您一周愉快!

—Carl Lerche