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。