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

相关推荐
2401_8576100330 分钟前
SpringBoot社团管理:安全与维护
spring boot·后端·安全
凌冰_1 小时前
IDEA2023 SpringBoot整合MyBatis(三)
spring boot·后端·mybatis
码农飞飞1 小时前
深入理解Rust的模式匹配
开发语言·后端·rust·模式匹配·解构·结构体和枚举
一个小坑货1 小时前
Rust 的简介
开发语言·后端·rust
qq_172805591 小时前
RUST学习教程-安装教程
开发语言·学习·rust·安装
monkey_meng2 小时前
【遵守孤儿规则的External trait pattern】
开发语言·后端·rust
Estar.Lee2 小时前
时间操作[计算时间差]免费API接口教程
android·网络·后端·网络协议·tcp/ip
新知图书3 小时前
Rust编程与项目实战-模块std::thread(之一)
开发语言·后端·rust
盛夏绽放3 小时前
Node.js 和 Socket.IO 实现实时通信
前端·后端·websocket·node.js
Ares-Wang3 小时前
Asp.net Core Hosted Service(托管服务) Timer (定时任务)
后端·asp.net