你好 Tokio

我们将从编写一个非常基础的 Tokio 应用程序开始。它将连接到 Mini-Redis 服务器,并将键 hello 的值设置为 world。然后它将读取回该键。这将使用 Mini-Redis 客户端库完成。

代码

生成一个新的 crate

让我们从生成一个新的 Rust 应用程序开始

$ cargo new my-redis
$ cd my-redis

添加依赖项

接下来,打开 Cargo.toml 并在 [dependencies] 下面添加以下内容

tokio = { version = "1", features = ["full"] }
mini-redis = "0.4"

编写代码

然后,打开 main.rs 并将文件内容替换为

use mini_redis::{client, Result};

#[tokio::main]
async fn main() -> Result<()> {
    // Open a connection to the mini-redis address.
    let mut client = client::connect("127.0.0.1:6379").await?;

    // Set the key "hello" with value "world"
    client.set("hello", "world".into()).await?;

    // Get key "hello"
    let result = client.get("hello").await?;

    println!("got value from the server; result={:?}", result);

    Ok(())
}

确保 Mini-Redis 服务器正在运行。在一个单独的终端窗口中,运行

$ mini-redis-server

如果您尚未安装 mini-redis,可以使用以下命令进行安装

$ cargo install mini-redis

现在,运行 my-redis 应用程序

$ cargo run
got value from the server; result=Some(b"world")

成功!

您可以在这里找到完整代码。

分解

让我们花一些时间来回顾一下我们刚刚做了什么。代码不多,但发生了很多事情。

let mut client = client::connect("127.0.0.1:6379").await?;

client::connect 函数由 mini-redis crate 提供。它异步地建立与指定远程地址的 TCP 连接。连接建立后,将返回一个 client 句柄。即使操作是异步执行的,我们编写的代码看起来是同步的。异步操作的唯一指示是 .await 运算符。

什么是异步编程?

大多数计算机程序都按照编写的顺序执行。第一行执行,然后是下一行,依此类推。使用同步编程,当程序遇到无法立即完成的操作时,它将阻塞直到操作完成。例如,建立 TCP 连接需要与网络上的对等方进行交换,这可能需要相当长的时间。在此期间,线程被阻塞。

使用异步编程,无法立即完成的操作将被挂起至后台。线程不会被阻塞,并且可以继续运行其他操作。一旦操作完成,任务将被取消挂起,并从上次停止的地方继续处理。我们之前的示例只有一个任务,因此在挂起期间不会发生任何事情,但异步程序通常有许多这样的任务。

尽管异步编程可以提高应用程序的速度,但它通常会导致程序变得更加复杂。程序员需要跟踪恢复异步操作完成后工作所需的所有状态。从历史上看,这是一项繁琐且容易出错的任务。

编译时绿色线程

Rust 使用一个名为 async/await 的功能来实现异步编程。执行异步操作的函数用 async 关键字标记。在我们的示例中,connect 函数的定义如下

use mini_redis::Result;
use mini_redis::client::Client;
use tokio::net::ToSocketAddrs;

pub async fn connect<T: ToSocketAddrs>(addr: T) -> Result<Client> {
    // ...
}

async fn 定义看起来像一个常规的同步函数,但以异步方式运行。Rust 在编译时将 async fn 转换为异步运行的例程。对 async fn 中的 .await 的任何调用都会将控制权返回给线程。线程可以在后台处理操作时执行其他工作。

尽管其他语言也实现了 async/await,但 Rust 采用了独特的方法。主要是,Rust 的异步操作是惰性的。这导致与其他语言不同的运行时语义。

如果这还不太清楚,请不要担心。我们将在本指南中更深入地探讨 async/await

使用 async/await

异步函数的调用方式与任何其他 Rust 函数一样。但是,调用这些函数不会导致函数体执行。相反,调用 async fn 会返回一个表示操作的值。这在概念上类似于零参数闭包。要实际运行操作,您应该对返回值使用 .await 运算符。

例如,给定的程序

async fn say_world() {
    println!("world");
}

#[tokio::main]
async fn main() {
    // Calling `say_world()` does not execute the body of `say_world()`.
    let op = say_world();

    // This println! comes first
    println!("hello");

    // Calling `.await` on `op` starts executing `say_world`.
    op.await;
}

输出

hello
world

async fn 的返回值是一个匿名类型,它实现了 Future trait。

异步 main 函数

用于启动应用程序的 main 函数与 Rust 大多数 crates 中常见的 main 函数不同。

  1. 它是一个 async fn
  2. 它使用 #[tokio::main] 注解

使用 async fn 是因为我们想要进入异步上下文。但是,异步函数必须由运行时执行。运行时包含异步任务调度器,提供事件驱动的 I/O、定时器等。运行时不会自动启动,因此 main 函数需要启动它。

#[tokio::main] 函数是一个宏。它将 async fn main() 转换为同步的 fn main(),后者初始化运行时实例并执行异步 main 函数。

例如,以下代码

#[tokio::main]
async fn main() {
    println!("hello");
}

被转换为

fn main() {
    let mut rt = tokio::runtime::Runtime::new().unwrap();
    rt.block_on(async {
        println!("hello");
    })
}

Tokio 运行时的详细信息将在稍后介绍。

Cargo features

当依赖 Tokio 进行本教程时,启用了 full feature flag

tokio = { version = "1", features = ["full"] }

Tokio 具有很多功能(TCP、UDP、Unix 套接字、定时器、同步实用程序、多种调度器类型等)。并非所有应用程序都需要所有功能。当尝试优化编译时间或最终应用程序 footprint 时,应用程序可以决定只选择它使用的功能。