你好 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 函数不同。
- 它是一个
async fn
- 它使用
#[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 时,应用程序可以决定只选择它使用的功能。