Hello Tokio

编写一个非常基本的Tokio应用程序

https://tokio.rs/tokio/tutorial/hello-tokio

我们将通过编写一个非常基本的Tokio应用程序开始。它将连接到Mini-Redis服务器,将 key hello的值设置为world。然后它将读回key。这将使用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 转化为一个异步运行的routine。在 async fn 中对 .await 的任何调用都会将控制权交还给线程。当操作在后台进行时,线程可以做其他工作。

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

如果这还不是很有意义,不要担心。我们将在本指南中更多地探讨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工具箱中的常见函数不同。

  • 它是 async fn
  • 它被注解为 #[tokio::main]

使用 async fn 是因为我们想进入一个异步上下文。然而,异步函数必须由一个运行时来执行。运行时包含异步任务调度器,提供事件化I/O、计时器等。运行时不会自动启动,所以主函数需要启动它。

#[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 的功能标志:

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

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

目前,在依赖 tokio 时,请使用 “full” feature。