Rust 学习笔记:Future trait 和 Async 语法

Rust 学习笔记:Future trait 和 Async 语法

预备知识:并发与并行

并发:单个执行单元处理多个任务,通过任务切换实现

在这里插入图片描述

并行:多个执行单元同时处理多个任务

在这里插入图片描述

串行:任务必须按特定顺序一个接一个完成

如果任务之间存在依赖关系,可以将并行与串行相结合:

在这里插入图片描述

在具有单个 CPU 核心的机器上,CPU 一次只能执行一个操作,但它仍然可以并发地工作。使用线程、进程和异步等工具,计算机可以暂停一个活动并切换到其他活动,然后再最终循环回到第一个活动。

在具有多个 CPU 核心的机器上,它还可以并行工作。一个核心可以执行一个任务,而另一个核心执行一个完全不相关的任务,这些操作实际上是同时发生的。

异步编程的关键元素

Rust 中异步编程的关键元素是 future 和 Rust 的 async 和 await 关键字。

future(未来量)是一种可能现在还没有准备好,但在未来的某个时候会准备好的值。

Rust 提供了一 个Future trait 作为构建块,可以为自定义数据类型实现 Future trait,这样不同的异步操作可以用不同的数据结构实现,但有一个通用的接口。在 Rust 中,future 是实现 Future trait 的类型。每个 future 都有自己的信息,关于已经取得的进展和“准备好”的含义。

async 关键字用于代码块或函数,表示可被中断和恢复。

await 关键字用于等待 future 就绪。Rust 提供暂停和恢复执行的点,通过轮询的方法检查 future 值是否可用。

Rust 编译器将 async/await 的代码块或函数转换为使用 Future trait 的等效代码,返回 future。类似于 for 循环被转换为使用 Iterator trait。

编写异步程序

创建项目 hello-async。

trpl crate

Rust 官方提供了 trpl crate 让我们专注于异步编程学习,不受生态系统干扰。trpl crate 整合了我们需要的类型、trait 和函数, 主要来自于 futures 和 Tokio 这两个核心异步库。

futures crate 是用于异步代码的 Rust 实验的官方主页,它实际上是 Future trait 最初设计的地方。Tokio 是目前 Rust 中使用最广泛的异步运行时,特别是对于 Web 应用程序。

添加 trpl crate 的命令:

cargo add trpl

Rust 官方默认的 Cargo 源服务器为 crates.io,其同时也是 Rust 官方的 crate 管理仓库,但是由于官方服务器部署在北美洲,中国大陆用户下载速度较慢,甚至反复中断下载。

warning: spurious network error (3 tries remaining): failed to receive response

参考文章:https://blog.csdn.net/qq_28550263/article/details/130758057

省流版:

  1. 打开 VS Code,在终端输入 code $HOME/.cargo/config

在这里插入图片描述

  1. 输入以下内容,使用字节跳动镜像源
[source.crates-io]
# To use sparse index, change 'rsproxy' to 'rsproxy-sparse'
replace-with = 'rsproxy'

[source.rsproxy]
registry = "https://rsproxy.cn/crates.io-index"
[source.rsproxy-sparse]
registry = "sparse+https://rsproxy.cn/index/"

[registries.rsproxy]
index = "https://rsproxy.cn/crates.io-index"

[net]
git-fetch-with-cli = true
  1. config 创建成功

在这里插入图片描述

再执行 cargo add 命令就成功了。

在这里插入图片描述

现在我们可以使用 trpl 提供的各个部分来编写第一个异步程序。我们将构建一个小的命令行工具,它获取两个网页,从中提取 <title> 元素,并打印出先完成整个过程的页面的标题。

定义 page_title 函数

让我们从编写一个函数开始,该函数接受一个页面 URL 作为参数,向它发出请求,并返回 title 元素的文本。

use trpl::Html;

async fn page_title(url: &str) -> Option<String> {
    let response = trpl::get(url).await;
    let response_text = response.text().await;
    Html::parse(&response_text)
        .select_first("title")
        .map(|title_element| title_element.inner_html())
}

首先,定义一个名为 page_title 的函数,并用 async 关键字标记它。然后我们使用 trpl::get 函数来获取传入的 URL,并添加 await 关键字来等待响应。为了获得响应的文本,我们调用它的 text 方法,并再次使用 await 关键字等待它。这两个步骤都是异步的。

对于 get 函数,我们必须等待服务器发回其响应的第一部分,其中将包括 HTTP 标头、cookie 等,并且可以与响应体分开交付。特别是如果 response body 非常大,它可能需要一些时间才能全部到达。因为我们必须等待整个响应到达,所以 text 方法也是异步的。

我们必须显式地等待这两个 future,因为 Rust 中的 future 是懒惰的:它们不会做任何事情,直到你用 await 关键字请求它们。

这类似于迭代器,它什么也不做,除非你调用它们的 next 方法——无论是直接调用,还是通过使用 for 循环或 map 等在底层使用 next 的方法调用。

future 的惰性允许 Rust 在实际需要之前避免运行异步代码。

一旦有了 response_text,就可以使用 Html::parse 方法将其解析为 Html 类型的实例。我们使用 select_first 方法来查找给定 CSS 选择器的第一个实例。通过传递字符串 “title”,我们将获得文档中的第一个 <title> 元素(如果有的话)。因为可能没有任何匹配的元素,所以 select_first 返回 Option<ElementRef>。最后,我们使用 Option::map 方法,该方法允许我们处理 Option 中存在的项,如果不存在则不执行任何操作。在map 的闭包中,我们调用 inner_html 方法来获取 title_element 的内容,这是一个 String。当所有这些都说了并做了之后,我们有了一个 Option<String>。

因为 await 关键字是一个后缀关键字,它使方法链更易于使用。我们可以更改 page_title 的主体,将 trpl::get 和 text 函数调用与它们之间的 await 链接在一起。

let response_text = trpl::get(url).await.text().await;

当 Rust 看到用 async 关键字标记的块时,它将其编译成一个唯一的匿名数据类型,实现 Future trait。当 Rust 看到一个带有 async 标记的函数时,它将其编译成一个非 async 函数,其函数体是一个 async 块。异步函数的返回类型是编译器为该异步块创建的匿名数据类型的类型。

因此,编写 async fn 相当于编写一个返回该返回类型的 future 的函数。对于编译器来说,async fn page_title 函数定义等同于这样定义的非 async 函数:

use std::future::Future;
use trpl::Html;

fn page_title(url: &str) -> impl Future<Output = Option<String>> {
    async move {
        let text = trpl::get(url).await.text().await;
        Html::parse(&text)
            .select_first("title")
            .map(|title| title.inner_html())
    }
}

该函数使用了 impl trait 语法,返回的 trait 是一个 future,带有关联的 Output<String> 类型。

在原始函数体中调用的所有代码都包装在一个 async move 块中。

记住,块是表达式。这整个块是函数返回的表达式。

这个异步块产生一个 Output<String> 类型的值,与返回类型匹配。

编写 main 函数

获取命令行参数作为 url。

因为 future 生成的值是 Option<String>,所以我们使用 match 表达式来打印不同的消息,以说明页面是否具有 <title>。

async fn main() {
    let args: Vec<String> = std::env::args().collect();
    let url = &args[1];
    match page_title(url).await {
        Some(title) => println!("The title for {url} was {title}"),
        None => println!("{url} had no title"),
    }
}

不幸的是,main 函数不能被标记为 async,原因是异步代码需要一个 runtime(运行时):一个管理执行异步代码细节的 Rust crate。程序的 main 函数可以初始化运行时,但它本身并不是运行时。每个执行异步代码的 Rust 程序至少有一个地方可以设置运行时并执行 future。

修改代码,使用来自 trpl crate 的 run 函数,它将 future 作为参数并运行它直到完成。在幕后,调用 run 会设置一个运行时,用于运行传入的 future。一旦 future 完成,run 将返回 future 生成的任何值。

我们可以将 page_title 返回的 future 直接传递给 run,一旦它完成,我们就可以匹配结果 Option<String>。我们将传递一个 async 块并显式等待 page_title 调用的结果。

使用 cargo run 命令,带上 URL 参数:cargo run -- https://www.rust-lang.org

在这里插入图片描述

每个等待点(即代码使用 await 关键字的每个位置)都表示将控制权交还给运行时的位置。要做到这一点,Rust 需要跟踪异步块中涉及的状态,以便运行时可以启动其他工作,然后在准备好再次尝试推进第一个工作时返回。这是一个看不见的状态机,就好像你写了一个枚举来保存每个等待点的当前状态:

enum PageTitleFuture<'a> {
    Initial { url: &'a str },
    GetAwaitPoint { url: &'a str },
    TextAwaitPoint { response: trpl::Response },
}

然而,手工编写在每个状态之间转换的代码将是乏味且容易出错的,特别是当你需要稍后向代码中添加更多功能和更多状态时。幸运的是,Rust 编译器会自动创建和管理异步代码的状态机数据结构。数据结构周围的正常借用和所有权规则仍然适用,编译器还为我们处理检查这些规则并提供有用的错误消息。

最终,必须有东西执行这个状态机,而这个东西就是运行时。

这就是为什么在查看运行时时可能会遇到对执行器的引用:执行器是运行时中负责执行异步代码的部分。

这解释了为什么编译器阻止我们将 main 本身设置为 async 函数。如果 main 是一个异步函数,则需要其他东西来管理状态机,以处理将来 main 返回的任何内容,但 main 是程序的起点!相反,我们在 main 中调用 trpl::run 函数来设置一个运行时,并运行异步块返回的 future,直到它完成。

编写并发程序

编写一个并发程序,使用从命令行传入的两个不同 url 调用 page_title 函数,并且它们并发执行,谁先执行完就打印谁的 title。

use trpl::{Either, Html};

fn main() {
    let args: Vec<String> = std::env::args().collect();

    trpl::run(async {
        let title_fut_1 = page_title(&args[1]);
        let title_fut_2 = page_title(&args[2]);

        let (url, maybe_title) =
            match trpl::race(title_fut_1, title_fut_2).await {
                Either::Left(left) => left,
                Either::Right(right) => right,
            };

        println!("{url} returned first");
        match maybe_title {
            Some(title) => println!("Its page title is: '{title}'"),
            None => println!("Its title could not be parsed."),
        }
    })
}

async fn page_title(url: &str) -> (&str, Option<String>) {
    let text = trpl::get(url).await.text().await;
    let title = Html::parse(&text)
        .select_first("title")
        .map(|title| title.inner_html());
    (url, title)
}

我们首先为每个 url 调用 page_title 函数。我们将结果 future 保存为 title_fut_1和 title_fut_2。记住,这些还没有做任何事情,因为 future 是懒惰的。然后我们将 future 传递给 trpl::race,它返回一个值来指示传递给它的 future 中哪个先完成。

任何一个 future 都可以合法地先得到结果,因此返回 Result 没有意义。相反,race 返回一个我们以前没有见过的类型,trpl::Either。Either 类型有点类似于 Result,因为它有两种情况。与 Result 不同的是,“要么”中没有成功或失败的概念。相反,它使用 Left 和 Right 来表示“一个或另一个”:

enum Either<A, B> {
    Left(A),
    Right(B),
}

race 函数将首先完成的第一个 future 参数的输出返回 Left,如果第二个 future 参数先完成,则返回 Right。

我们还更新 page_title 函数以返回传入的 URL。这样,如果第一个返回的页面没有可以解析的 <title>,仍然可以打印有意义的消息。有了这些信息,我们就可以通过 println! 输出以指示哪个 URL 先完成,以及该 URL 上的网页的 <title> 是什么(如果有的话)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

UestcXiye

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值