Rust Async Await 入门
在本文中,我们将仔细研究 Rust 中的异步编程。到目前为止,我对 Rust 异步的体验主要是从 Stack Overflow 复制代码。本文旨在帮助您了解什么是异步代码以及如何有效地使用它。
什么是异步代码?
要了解什么是异步代码,我们首先来谈谈同步代码。
在同步代码中,语句按顺序运行:
rust
println!("Hello World");
let cargo_toml_content = std::fs::read_to_string("Cargo.toml").unwrap();
println!("'Cargo.toml':\n{}", cargo_toml_content);
上述语句按照明确的顺序执行,从上到下一个接一个地执行。" 打印 Hello World
,然后读取并打印 Cargo.toml
的内容。
这种模式在正常操作下非常好 - 但有时代码需要在当前上下文在等待其他内容时暂停 - 这通常称为阻塞。换句话说,当一段代码被阻塞时,它实际上处于暂停状态,等待特定操作完成才能继续。例如,当等待文件系统、网络通信、数据库事务甚至一段时间过去时,就会发生这种情况。在此阻塞状态期间,程序保持空闲状态,无法同时执行其他任务。在前面的示例中,循环无法继续进行下一次迭代,直到上一次迭代中的请求完成为止。这可能会导致效率低下,尤其是在处理大量此类请求时。
在下面的示例中,每次循环迭代都会向 example.com
发出请求。
rust
for index in 1..=100 {
let result = sync_http_client.get(format!("www.example.com/items/{}", index));
}
这里的问题是 sync_http_client.get
被阻塞。发生阻塞的原因有很多:
- 等待文件系统
- 等待网络
- 等待一些数据库事务
- 等待一段时间发生
- 其他情况。
当程序被阻塞时,它什么也不做,只是等待响应返回以继续执行。如果我们需要做其他事情------我们就会陷入困境。在此示例中,循环无法运行下一次迭代,直到前一个迭代中的请求完全完成。虽然发出和读取单个请求相对较快,但循环中的代码运行 100 次并发出 100 个请求,因此整个循环需要一段时间才能运行。
如果有一种方法可以启动其他请求而不必等待前一个请求完成,该怎么做呢?
这就是异步编程的用武之地。异步编程就是非阻塞。假设您订购了一辆山地自行车以供周末骑行。您无需将所有时间都花在门口等待送货 - 您可以继续生活,做任何事情。异步运行时允许您继续正在做的任何事情,并作为通知,在送货到达门口时将提醒您。
稍后我们将详细介绍如何编写异步,但本质是我们可以将循环更改为以下内容以启动 100 个请求,而不需要等待完成前一个请求:
rust
let mut handles = Vec::new();
for index in 1..=100 {
let handle = tokio::spawn(
async_http_client.get(format!("www.example.com/items/{}", index))
);
handles.push(handle);
}
for handle in handles {
let result = handle.await;
}
并行和并发 (Parallelization and concurrency)
在我们进一步讨论之前,我们应该注意 异步不适用于cpu 密集的操作。它仅对数据来自比 RAM 更远的地方并且是IO密集型时 有利。并行对于 cpu密集型 较高的操作是有益的。
** 并行是同时运行多个事物。并发是同时处理多个事情。**
并发与并行的区别: 异步是为并发(Concurrency)而设计的。 Tokio 的默认运行时使用线程,因此我们也可以从并行化中受益。
- 并发(Concurrent) 是多个队列使用同一个咖啡机,然后两个队列轮换着使用(未必是 1:1 轮换,也可能是其它轮换规则),最终每个人都能接到咖啡
- 并行(Parallel) 是每个队列都拥有一个咖啡机,最终也是每个人都能接到咖啡,但是效率更高,因为同时可以有两个人在接咖啡
Benchmarking 测试基准
比较使用异步编写的示例与同步编写的相同示例 - 对于大量并发 Web 请求,异步版本比同步请求快约 60%,比为每个请求旋转线程 1
Command | Mean [s] | Min [s] | Max [s] | Relative |
---|---|---|---|---|
./sync |
1.070 ± 0.013 | 1.060 | 1.085 | 1.65 ± 0.09 |
./threads |
0.787 ± 0.007 | 0.782 | 0.795 | 1.22 ± 0.06 |
./async |
0.732 ± 0.016 | 0.721 | 0.750 | 1.13 ± 0.06 |
./async_threads |
0.646 ± 0.033 | 0.612 | 0.677 | 1.00 |
Rust 异步编程入门
Rust 没有运行时 2 ,因此没有标准执行器(至少目前如此)。有几种流行的执行器运行时。这些是像任何其他库一样的crate,因此您可以通过将它们添加到 Cargo.toml
来使用它们。对于这个演示,我们将选择 Tokio Rust
(Tokio-rs) - tokio.rs/ 作为最受欢迎的执行器。存在其他运行时并优先考虑不同的事情。例如,async-std
专注于 Rust 标准库的异步版本,而 smol
专注于轻量级。总体而言,Rust 的设计目的是避免干扰,因此它可以让您选择运行哪个执行程序。
首先,我们将运行 cargo new
。然后将 tokio = { version = "1.19", features = ["full"] }
添加到 Cargo.toml
(或者如果您安装了 Cargo-edit: cargo add tokio -F full
)
rust
#[tokio::main]
async fn main() {
println!("Hello from an async function");
}
Async functions 异步函数
在 Rust 中,包含异步操作的函数由 async
关键字标识。要声明这样的函数,只需在其前面加上 async
前缀,如下所示:
rust
async fn do_thing() {
let result = some_async_function().await;
println!("{}", result);
}
在异步函数中,您可以使用 .await
。它被写到异步函数调用的结尾,它在非阻塞执行中起着至关重要的作用。当您使用 .await
时,它会暂时停止执行并获取实际结果值。
现在,让我们更深入一点。异步函数以及异步块返回 Futures。 Future 是一个返回 Poll 的函数。 Poll 有点像 Result
或 Option
,它有两种变体,一种是 最终结果,另一种是该值仍处于阻塞状态。 Future 是惰性的,有两种方法可以运行 future: tokio::spawn
立即生成并获取 JoinHandle
或 .await
。 Rust 会警告 unawaited futures。
编写异步操作
让我们看看下面的代码片段:
rust
let contents = tokio::fs::read("Cargo.toml").await;
在此代码片段中,您可能会对 tokio::fs::read
及其与 Rust 标准库中的 std::fs::read
函数的相似之处感到好奇。这就是 Tokio 证明其实用性的地方。 Tokio 提供了 Rust 标准库中同步输入和输出 (IO) 操作的异步对应项。具体来说, tokio::fs::read
表示异步文件读取操作。它的特别之处在于它的异步特性;它使您的程序能够读取文件内容而不阻塞其他任务。在等待文件读取完成时,您的程序可以继续同时执行其他任务。这种非阻塞行为是 Rust 异步编程的一个基本方面,可以保护您的程序在 IO 操作期间不会无响应。
Writing concurrency 并发写入
如前所述,阻塞调用的问题在于它们一次只允许运行一个任务。
rust
let weather = client.get("https://api.darksky.net/forecast").await;
let news = client.get("https://api.nytimes.com/svc/topstories").await;
使用 tokio::join!
,我们可以同时发起两个请求并等待它们的结果。
rust
let weather = client.get("https://api.darksky.net/forecast");
let news = client.get("https://api.nytimes.com/svc/topstories");
let (weather, news) = tokio::join!(weather, news).await;
tokio::join
!同时启动多个异步任务,然后同时等待它们的结果。本质上,它同时启动天气和新闻请求,然后等待两个响应,而不是等待一个响应完成后再启动另一个响应。这种并发方法与顺序执行有很大不同,在顺序执行中,您首先请求天气,然后等待其完成,并且只有在请求新闻之后才执行。通过利用 tokio::join!
,您可以有效地利用程序的时间,从而提高处理多个异步操作时的性能。
为了保持这篇文章的简短和基础知识,我们将停在这里。如果您想了解有关编写异步的更多信息,可以阅读 Rust 异步官方书籍,并且 Tokio 有精彩的教程。
Conclusion 总结
Rust Async 是 Rust 语言的一个实用且不断发展的方面。虽然异步功能不断发展,但未来仍有改进的空间。您可以在 areweasyncyet.rs 上检查异步功能的当前状态以及异步生态系统的其他方面。这篇文章提供了编写异步 Rust 代码的介绍性指南,因此您绝对可以等待未来的文章,深入探讨 Rust 中的异步,主题包括: Rust 流、异步代码中的错误处理、高级并发模式以及实际应用程序中异步 Rust 的实际示例。
脚注
-
我们在展示异步的有益结果方面遇到了一些困难,并且仍然不确定这些结果是否很好地反映了异步的好处。您可以在此处查看结果,并在此处查看完整的基准测试代码here。 ↩
-
从技术上讲,有恐慌处理程序和The Rust runtime - The Rust Reference ↩