Rust异步编程 Async Await 入门

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 有点像 ResultOption ,它有两种变体,一种是 最终结果,另一种是该值仍处于阻塞状态。 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 的实际示例。


脚注

  1. 我们在展示异步的有益结果方面遇到了一些困难,并且仍然不确定这些结果是否很好地反映了异步的好处。您可以在此处查看结果,并在此处查看完整的基准测试代码here。 ↩

  2. 从技术上讲,有恐慌处理程序和The Rust runtime - The Rust Reference

原文连接:Getting started with Async Rust

相关推荐
kfaino1 分钟前
码农的AI翻身(六)你好,我叫 Parameter
后端·aigc
掘金者阿豪4 分钟前
把业务数据变成共享仪表盘:Metabase可视化与远程访问实践
前端·后端
猪猪拆迁队1 小时前
虚拟工厂仿真引擎的架构设计:让一条产线可编程、可观测、可干预
后端·ai编程
字节跳动数据库2 小时前
文章分享——相似函数处理方法
人工智能·后端·程序员
云技纵横2 小时前
@Transactional 失效的 7 种场景:第 5 种最难排查
后端
用户6757049885022 小时前
你知道 Go 结构体和结构体指针调用的区别吗?一文带你彻底搞懂!
后端·go
程序员cxuan2 小时前
读懂 Claude Code 架构分析系列,第一篇,开始!
人工智能·后端·架构
用户6757049885022 小时前
面试官问“装饰器模式”,这样回答薪资多要 3000!
后端
tntxia2 小时前
Geo Scene域名修改引起的一些问题
后端
用户298698530142 小时前
Java 实现 Word 文档加密与权限解除
java·后端