七爪源码:Rust 异步第 3 部分

本系列的第 2 部分使用一个简单的示例来解释异步 Rust 的基本原理和组件。 在这篇文章中,我们将演示如何包含一个单独的运行时 crate 并使用它来运行并发任务,并简要比较当前选项。

七爪源码:Rust 异步第 3 部分

当我们上次离开时,我们使用的是 futures crate 中可用的原始执行器,有时称为 futures-rs。 futures crate 为 Rust 语言中的异步行为提供了通用抽象层。 它旨在成为创建复杂运行时的基础。 如前几篇文章所述,这是设计使然。 如果您打算从头开始创建自定义运行时、执行程序等,您可能不会直接使用期货。

我选择在这个例子中使用 async-std crate 只是因为任何已经熟悉 Rust std 库的人都可以轻松阅读它。 我还将使用计时器来证明我们实际上是按预期以并发方式运行的。


首先,我们将按如下方式设置 cargo.toml 文件。

[package]
name = "rust-async"
version = "0.1.0"
edition = "2021"
async-std = "1.12.0"
futures = "0.3"
rand = "0.8.5"

我只是重用了原始 cargo.toml 文件并将 async-std 和 rand crates 添加为依赖项。 我们添加了 rand crate,以便我们可以为我们的函数生成随机睡眠时间。

接下来,我们将 main.rs 文件更改为如下所示:

use std::time::{Instant, Duration};
use async_std::task;
use rand::Rng;fn main() {
    let start_time = Instant::now();    task::block_on(async_main());
    print!("Completed in {} ms", start_time.elapsed().as_millis());}async fn async_main() {
    let long_running_stuff = make_coffee_and_bacon();
    let other_stuff = make_eggs();
    futures::join!(long_running_stuff, other_stuff);
}async fn make_coffee_and_bacon() {
    futures::join!(make_coffee(), make_bacon());
}async fn make_coffee() {
    burning_time("Coffee".to_string()).await;
}async fn make_eggs() {
    burning_time("Eggs".to_string()).await;
}async fn make_bacon() {
    burning_time("Bacon".to_string()).await;
}async fn burning_time(breakfast_item: String) {
    let mut rng = rand::thread_rng();    println!("{} started!", breakfast_item);
    let sleep_duration =     Duration::from_millis(rng.gen_range(100..1500));    task::sleep(sleep_duration).await;    println!("{} finished after {:?} ms", breakfast_item, sleep_duration.as_millis());
}

在我们进行了所有修改之后,这看起来与我们在上一篇文章结尾处的代码的最终版本非常相似。我们所做的一组更改是从 use 语句开始的。我们从 Rust 标准库导入 Instant 和 Duration。我们使用它们来计时代码的执行,以便我们可以证明函数是同时运行的。我们不导入期货箱,而是导入 async-std 箱。我们还导入 rng::Rng 以便我们可以生成一些随机时间。

除了创建计时器和打印总执行超时之外,对 main 的唯一更改是,我们没有使用 futures crate 中的 block_on() 执行程序,而是使用 async-std crate 中的 task::block_on() 接口。它的行为与我们在 futures crate 中看到的类似,但执行器更复杂,因为当任务阻塞当前线程时,它也同时运行所有异步代码。如果一个异步函数无法完成,例如,它处于睡眠状态,它将把控制权交给一个可以取得进展的任务。顺便说一句,如果我们使用阻塞接口来运行并发任务看起来很奇怪,原因是我们不希望程序在所有任务被驱动以某种形式完成之前退出。

如果您编译并运行该程序,您将看到类似于此处显示的结果:

└─[0]  cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.03s
     Running `target/debug/rust-async`
Coffee started!
Bacon started!
Eggs started!
Bacon finished after 211 ms
Eggs finished after 430 ms
Coffee finished after 1318 ms
Completed in 1319 ms%
┌─[clayratliff@pop-os] - [~/PersonalGithub/rust-async] - [2022-07-01 05:59:36]
└─[0]  cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.03s
     Running `target/debug/rust-async`
Coffee started!
Bacon started!
Eggs started!
Coffee finished after 255 ms
Eggs finished after 1429 ms
Bacon finished after 1499 ms
Completed in 1500 ms%

从结果中我们可以看到,虽然早餐项目总是以相同的顺序开始,但它们显然是同时运行的,因为它们完成的顺序会根据它们的随机睡眠时间而变化。 我们还可以看到,代码的总运行时间总是最长的睡眠时间加上一两毫秒的时间长度。

最后,让我们更进一步,创建一个看起来更像真实世界应用程序的东西。 我们将运行多个并发任务以从 Web 获取一些数据并返回 Result,这是 I/O 的一个极其常见的返回值。

首先,让我们在 cargo.toml 文件中添加一些需要的 crate:

[package]
name = "rust-async"
version = "0.1.0"
edition = "2021"# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html[dependencies]
async-std = "1.12.0"
futures = "0.3"
serde = "1.0.138"
surf = "2.3.2"

我们已经熟悉了上一篇文章中的 async-std 和 futures crate。 我们将为示例提取的数据将是 JSON 格式的股票行情数据。 Serde 是 Rust 中用于序列化和反序列化数据结构的框架。 可以在链接中找到更多详细信息,但我们将使用它来反序列化 JSON 数据。 我们将从 Alpha Vantage 中提取我们的代码数据。 主要是因为注册又快又容易。 它也不需要任何费用,但确实为我提供了可以使用的实时数据。 JSON 数据的示例如下所示:

{
    "Global Quote": {
        "01. symbol": "IBM",
        "02. open": "141.0000",
        "03. high": "141.6700",
        "04. low": "139.2600",
        "05. price": "141.1200",
        "06. volume": "4012106",
        "07. latest trading day": "2022-07-01",
        "08. previous close": "141.1900",
        "09. change": "-0.0700",
        "10. change percent": "-0.0496%"
    }
}

Surf 是一个 HTTP 客户端框架,可以很好地与 async-std 配合使用,并且可以方便地与 serde 集成以处理 JSON 文档。

现在让我们看看我们在 main.rs 中的简单股票代码示例

use async_std::task;
use futures::join;
use serde::Deserialize;
use std::time::Instant;
use surf::Result;#[derive(Deserialize)]
struct GlobalQuote {
    #[serde(rename = "01. symbol")]
    symbol: String,
    #[serde(rename = "02. open")]
    open: String,
    #[serde(rename = "03. high")]
    high: String,
    #[serde(rename = "04. low")]
    low: String,
    #[serde(rename = "05. price")]
    price: String,
}#[derive(Deserialize)]
struct TickerData {
    #[serde(rename = "Global Quote")]
    full_ticker: GlobalQuote,
}fn main() {
    let start_time = Instant::now();
    task::block_on(async_main());
    println!("Completed in {} ms", start_time.elapsed().as_millis());
}async fn async_main() {
    let api_key = "API_KEY";
    let ibm_symbol = format!(
        "https://www.alphavantage.co/query?function=GLOBAL_QUOTE&symbol=IBM&apikey={}",
        api_key
    );
    let tesla_symbol = format!(
        "https://www.alphavantage.co/query?function=GLOBAL_QUOTE&symbol=TSLA&apikey={}",
        api_key
    );
    let apple_symbol = format!(
        "https://www.alphavantage.co/query?function=GLOBAL_QUOTE&symbol=AAPL&apikey={}",
        api_key
    );
    let first_symbol = get_ticker(ibm_symbol);
    let second_symbol = get_ticker(tesla_symbol);
    let third_symbol = get_ticker(apple_symbol);    let (result1, result2, result3) = join!(first_symbol, second_symbol, third_symbol);
    
    println!("IBM ticker price: {}", result1.unwrap().price);
    println!("Tesla ticker price: {}", result2.unwrap().price);
    println!("Apple ticker price: {}", result3.unwrap().price);
}async fn get_ticker(url: String) -> Result {
    let start_time = Instant::now();
    let TickerData { full_ticker } = surf::get(&url).recv_json().await?;
    println!("Async completed in {} ms", start_time.elapsed().as_millis());
    Ok(full_ticker)
}

结构 GlobalQuote 和 TickerData 将保存反序列化的数据。 #[derive(Deserialize)] 注释处理实现 serde 的 Deserialize 特征。 #[serde(rename = "")] 做了你可能期望它做的事情;它将引号内的 JSON 字段名称映射到结构中的字段名称。

主要功能根本没有改变。 async_main 中的前 4 行只是设置了股票数据 API 的 URI。然后,我们为我们想要获取信息的三个股票代码创建一个 Future,创造性地命名 first_symbol、second_symbol 和 third_symbol。然后我们将这些 Future 传递给 join!宏,我们在上一篇文章中也看到过。最后,我们打印出最终的股票价格。

真正的肉,如果你可以这么说的话,在于微不足道的 get_ticker() 函数。返回的 Result<> 来自 surf 框架,是标准库 Result 的包装器。我们返回 GlobalQuote 是因为我们想将请求中的实际股票数据返回给调用函数。 let TickerData { full_ticker } 语法表示 JSON 应该映射到指定的结构。在我们的例子中它不是超级有用,因为值都是字符串,但是如果返回的数据被输入为字符串以外的东西,例如,如果 JSON 文档价格是一个实际数字,我们可以将字段价格定义为 f64 和serde 会将其转换为适当的类型。

如果您创建一个帐户并将您生成的 API 密钥替换为代码中的占位符,这应该会立即运行并返回类似于您在下面看到的结果。

Async completed in 226 ms
Async completed in 232 ms
Async completed in 278 ms
IBM ticker price: 141.1200
Tesla ticker price: 681.7900
Apple ticker price: 138.9300
Completed in 281 ms

同样,您可以通过请求同时运行的时间来查看,并且完成的总时间在最长运行任务的几毫秒内。

在尝试用 Rust 理解它之前,我没有用其他语言做过任何重要的异步编程,我很难理解它是如何工作的。我最大的困难是一个误解,即基于 futures crate 的 Rust 中的 async/await 如何工作有一个明确的标准,并且所有运行时都只是更复杂的 futures 版本。事实并非如此。

异步 Rust 有三个主要的生态系统。这些生态系统是 Tokio、async-std 和 smol。我将简要介绍一下它们,并指出一些有趣的事情。本文提供的链接将为您提供更详细的信息。

Tokio 是最古老的,于 2016 年发布,是撰写本文时使用最广泛的运行时。它使用基于 mio 构建的自定义 I/O 特征。这意味着它需要一个兼容层来与期货集成。它可以通过许多实用程序进行配置,但作为一个大 crate 提供,尽管您可以使用功能标志使 crate 更轻。

Async-std 于 2019 年发布,目标是成为 Rust 标准库的完整运行时异步版本,并构建在 futures 之上。它还使用了 async-executor crate,即 smol 提供的执行器。

Smol 是最新的,创建于 2020 年初。它也是建立在期货之上的,由 async-std 的共同创建者 Stjepan Glavina 创建,也使用期货箱。它的目的是尽可能小,因此功能被分成许多不同的 crate。它还提供了一个与 tokio 兼容的 crate。


结论

我希望这会有所帮助,而不是让事情变得更加混乱。 我很感激你能做到这一点。 如果您发现我做错了,或者有什么要补充的,请在评论中告诉我。 最后,祝您有美好的一天!

关注七爪网,获取更多APP/小程序/网站源码资源!

发表评论
留言与评论(共有 0 条评论) “”
   
验证码:

相关文章

推荐文章