Rust 学习笔记:处理任意数量的 future

Rust 学习笔记:处理任意数量的 future

  • Rust 学习笔记:处理任意数量的 future
    • 竞争的 future
    • 将控制权交给运行时
    • 构建我们自己的异步抽象

Rust 学习笔记:处理任意数量的 future

当两个 future 切换到三个 future 时,我们也必须从使用 join 切换到使用 join3。每次我们改变想要加入的 future 的数量时,都必须调用不同的函数,这是很烦人的。

幸运的是,我们有一个宏形式的 join,可以向其传递任意数量的参数,它还处理等待 future 本身。

trpl::join!(tx1_fut, tx_fut, rx_fut);

将 future 推入集合,然后等待它们的部分或全部 future 完成是一种常见的模式。要检查某个集合中的所有 future,我们需要遍历并连接所有这些 future。trpl::join_all 函数接受任何实现了 Iterator trait 的类型。

let futures = vec![tx1_fut, rx_fut, tx_fut];
trpl::join_all(futures).await;

然而,上述代码不能通过编译。因为编译器会为每个异步块创建一个唯一的枚举,不能在数组中放入两个不同的手写结构体,同样的规则也适用于编译器生成的不同枚举。

你可能会想到使用 Enum 来表示 vector 中可能出现的每种类型,但我们在这里做不到。我们没有办法命名不同的类型,因为它们是匿名的。

真正的解决办法是使用 trait 对象,这可以让我们将这些类型产生的每个匿名 Future 视为相同的类型,因为它们都实现了 Future trait。

let futures =vec![Box::new(tx1_fut), Box::new(rx_fut), Box::new(tx_fut)];
trpl::join_all(futures).await;

不幸的是,这段代码仍然无法编译。事实上,对于第二个和第三个 Box::new 调用,我们得到了与之前相同的基本错误,以及引用 Unpin trait 的新错误。

首先,让我们通过显式注释 futures 变量的类型来修复 Box::new 调用上的类型错误。

let futures: Vec<Box<dyn Future<Output = ()>>> =vec![Box::new(tx1_fut), Box::new(rx_fut), Box::new(tx_fut)];

这个类型声明有点复杂,所以让我们从头开始:

  1. 最内在的类型是 futrue 本身。通过写 Future<Output = ()>,我们显式地注意到未来的输出是单位类型 ()。
  2. 然后我们用 dyn 注释这个特性,把它标记为动态的。
  3. 整个 trait 引用被包装在一个 Box 中。
  4. 最后,我们明确指出,futrue 是包含这些项目的 Vec。
use std::pin::Pin;// -- snip --let futures: Vec<Pin<Box<dyn Future<Output = ()>>>> =vec![Box::pin(tx1_fut), Box::pin(rx_fut), Box::pin(tx_fut)];

现在修复第二个错误。错误信息提示第一个异步块不实现 Unpin trait,建议使用 pin! 或 Box::pin 来解决它。

我们可以按照编译器的建议来解决问题。我们首先从 std::pin 导入 Pin。接下来,我们更新 future 的类型注释,用一个 Pin 包装每个 Box。最后,我们使用 Box::pin 来固定 future 本身。

现在,程序可以正常运行了。

使用 Pin<Box<T>> 会增加少量的开销,因为使用 Box 将这些 future 放在堆上——我们这样做只是为了使类型对齐。我们实际上并不需要堆分配,毕竟这些 future 是这个特定函数的局部。如前所述,Pin 本身是一种包装器类型,因此我们可以获得在 Vec 中使用单一类型的好处——这是我们使用 Box 的最初原因——而无需进行堆分配。我们可以使用 std::pin::pin 宏直接对每个 future 使用 Pin。

然而,我们仍然必须明确固定引用的类型;否则,Rust 仍然不知道将它们解释为动态 trait 对象,这是我们在 Vec 中需要的。因此,我们将 pin 添加到 std::pin 的导入列表中。然后,我们可以在定义每个 future 时使用 pin! 将其固定,并将 future 定义为包含对动态 future 类型的固定可变引用的 Vec。

use std::pin::{Pin, pin};// -- snip --let tx1_fut = pin!(async move {// --snip--});let rx_fut = pin!(async {// --snip--});let tx_fut = pin!(async move {// --snip--});let futures: Vec<Pin<&mut dyn Future<Output = ()>>> =vec![tx1_fut, rx_fut, tx_fut];

我们忽略了我们可能有不同的输出类型这一事实。例如:

        let a = async { 1u32 };let b = async { "Hello!" };let c = async { true };let (a_result, b_result, c_result) = trpl::join!(a, b, c);println!("{a_result}, {b_result}, {c_result}");

其中,a 的匿名 future 实现 Future<Output = u32>, b 的匿名 future 实现 Future<Output = &str>, c 的匿名 future 实现 Future<Output =bool>。

我们可以使用 trpl::join! 等待它们,因为它允许我们传入多个 future类型并生成这些类型的元组。我们不能使用 trpl::join_all,因为它要求传入的所有 future 都具有相同的类型。

这是一个基本的权衡:我们可以使用 join_all 处理动态数量的 future,只要它们都具有相同的类型,或者我们可以使用 join 函数或 join! 宏来处理固定数量的 future,即使它们有不同的类型。

竞争的 future

当我们使用名字带 join 的函数和宏时,我们需要在继续之前完成所有的 future。

trpl::race 则不同,传入的 future 将彼此竞争。

        let slow = async {println!("'slow' started.");trpl::sleep(Duration::from_millis(100)).await;println!("'slow' finished.");};let fast = async {println!("'fast' started.");trpl::sleep(Duration::from_millis(50)).await;println!("'fast' finished.");};trpl::race(slow, fast).await;

每个 future 在开始运行时打印一条消息,通过调用和等待 sleep 暂停一段时间,然后在结束时打印另一条消息。然后我们通过创建 slow 和 fast,等待其中一个 future 完成“比赛”。这里忽略了 trpl::race 返回的 Either 实例,因为我们不在乎。

注意,如果将 trpl::race 的参数变换顺序,则“started”消息的顺序将发生变化,尽管 fast future 总是先完成。这是因为这个特殊的 race 函数的实现是不公平的,它总是按照传入的顺序运行作为参数的 future。其他实现是公平的,将随机选择先轮询哪个 future。

在每个等待(await)点,如果等待的未来还没有准备好,Rust 会给运行时(runtime)一个暂停任务并切换到另一个任务的机会。反之亦然:Rust 只暂停异步块,并在等待点将控制权交还给运行时。等待点之间的一切都是同步的。

这意味着,如果你在一个异步块中做了一堆工作,而没有等待点,那么这个 future 将阻止任何其他 future 取得进展。

这被称为一个 future "饿死"其他 future。

在某些情况下,这可能不是什么大事。但是,如果正在进行某种昂贵的设置或长时间运行的工作,或者如果将来要无限期地执行某些特定的任务,则需要考虑何时何地将控制权交还给运行时。

同样的道理,如果你有长时间运行的阻塞操作,异步可以是一个有用的工具,为程序的不同部分提供相互关联的方法。

但是在这些情况下,如何将控制权交还给运行时呢?

将控制权交给运行时

让我们模拟一个长时间运行的操作。

fn slow(name: &str, ms: u64) {thread::sleep(Duration::from_millis(ms));println!("'{name}' ran for {ms}ms");
}

这段代码使用 std::thread::sleep 而不是 trpl::sleep,这样调用 slow 会阻塞当前线程。

使用 slow 来模拟在一对 future 中执行 CPU 密集型工作。

        let a = async {println!("'a' started.");slow("a", 30);slow("a", 10);slow("a", 20);trpl::sleep(Duration::from_millis(50)).await;println!("'a' finished.");};let b = async {println!("'b' started.");slow("b", 75);slow("b", 10);slow("b", 15);slow("b", 350);trpl::sleep(Duration::from_millis(50)).await;println!("'b' finished.");};trpl::race(a, b).await;

每个 future 只在执行了一堆缓慢的操作后才将控制权交还给运行时。如果你运行这段代码,你会看到这样的输出:

'a' started.
'a' ran for 30ms
'a' ran for 10ms
'a' ran for 20ms
'b' started.
'b' ran for 75ms
'b' ran for 10ms
'b' ran for 15ms
'b' ran for 350ms
'a' finished.

与前面的示例一样,a 和 b 的竞争仍然在 a 完成后立即结束。不过,这两个 future 之间并没有交集。a future 在等待 trpl::sleep 调用之前完成所有的工作,然后 b future 在等待自己的 trpl::sleep 调用之前完成所有的工作,最后 a future 完成。

假如们在 a future 的末尾删除 trpl::sleep,它将在 b future 完全不运行的情况下完成。

为了允许两个 future 在它们缓慢的任务之间取得进展,我们需要等待点,这样我们就可以将控制权交还给运行时。这意味着我们需要一些可以等待的东西!

修改代码:

        let one_ms = Duration::from_millis(1);let a = async {println!("'a' started.");slow("a", 30);trpl::sleep(one_ms).await;slow("a", 10);trpl::sleep(one_ms).await;slow("a", 20);trpl::sleep(one_ms).await;println!("'a' finished.");};let b = async {println!("'b' started.");slow("b", 75);trpl::sleep(one_ms).await;slow("b", 10);trpl::sleep(one_ms).await;slow("b", 15);trpl::sleep(one_ms).await;slow("b", 350);trpl::sleep(one_ms).await;println!("'b' finished.");};

我们在每次调用之间添加带有等待点的 trpl::sleep 调用。现在,这两个 future 的工作交织在一起。每次其中一个 future 到达等待点时,另一个 future 就开始执行。

'a' started.
'a' ran for 30ms
'b' started.
'b' ran for 75ms
'a' ran for 10ms
'b' ran for 10ms
'a' ran for 20ms
'b' ran for 15ms
'a' finished.

每次想要切换 future 时就调用 trpl::sleep(one_ms).await 还是不够好,我们只需要把控制权交还给运行时。使用 yield_now 函数直接做到这一点。我们用 yield_now 替换了所有这些 trpl::sleep 调用。

        let a = async {println!("'a' started.");slow("a", 30);trpl::yield_now().await;slow("a", 10);trpl::yield_now().await;slow("a", 20);trpl::yield_now().await;println!("'a' finished.");};let b = async {println!("'b' started.");slow("b", 75);trpl::yield_now().await;slow("b", 10);trpl::yield_now().await;slow("b", 15);trpl::yield_now().await;slow("b", 350);trpl::yield_now().await;println!("'b' finished.");};

这段代码不仅更清楚地表明了实际意图,而且因为没有了 sleep,代码执行更快了。但要记住,yield_now 并不是免费的,仍存在细微的开销。

性能对比程序:

use std::time::{Duration, Instant};fn main() {trpl::run(async {let one_ns = Duration::from_nanos(1);let start = Instant::now();async {for _ in 1..1000 {trpl::sleep(one_ns).await;}}.await;let time = Instant::now() - start;println!("'sleep' version finished after {} seconds.",time.as_secs_f32());let start = Instant::now();async {for _ in 1..1000 {trpl::yield_now().await;}}.await;let time = Instant::now() - start;println!("'yield' version finished after {} seconds.",time.as_secs_f32());});
}

输出:

'sleep' version finished after 1.3959756 seconds.
'yield' version finished after 0.0003042 seconds.

可以看出 yield_now 的开销相比于 sleep 可以说是微乎其微。

通过调用 yield_now,每个 future 都有能力通过等待点决定何时移交控制权。因此,每个 future 也有责任避免阻塞太久。

构建我们自己的异步抽象

我们也可以共同构建未来,创造新的模式。例如,我们可以用已有的异步构建块构建超时函数。当我们完成后,结果将是另一个构建块,我们可以使用它来创建更多的异步抽象。

先介绍一下 timeout API:

async fn timeout<F: Future>(future_to_try: F,max_time: Duration,
) -> Result<F::Output, Duration> {match trpl::race(future_to_try, trpl::sleep(max_time)).await {Either::Left(output) => Ok(output),Either::Right(_) => Err(max_time),}
}

timeout 本身需要是一个 async 函数,这样我们才能等待它。它的第一个参数是要运行的任意类型的 future。它的第二个参数是最长等待时间。它返回一个 Result。如果 future 成功完成,结果就是 future 返回的值。如果超时,Result 将为 Err,并显示超时等待的持续时间。

我们想让 future_to_try 这个 future 与给定的时间 max_time 赛跑。trpl::race 是不公平的,按传递的顺序轮询参数。因此,我们首先将 future_to_try 传递给 race,这样即使 max_time 的持续时间非常短,它也有机会完成比赛。如果 future_to_try 先完成,race 将返回 Left,其中有 future_to_try 的输出。如果计时器先完成,race 将返回 Right,计时器的输出为 ()。如果 future_to_try 成功并且得到 Left(output),则返回 Ok(output)。如果睡眠计时器先结束,我们得到一个 Right(()),我们忽略其中的 () 并返回 Err(max_time)。

use std::time::Duration;
use trpl::Either;async fn timeout<F: Future>(future_to_try: F,max_time: Duration,
) -> Result<F::Output, Duration> {// skip
}fn main() {trpl::run(async {let slow = async {trpl::sleep(Duration::from_secs(5)).await;"Finally finished"};match timeout(slow, Duration::from_secs(2)).await {Ok(message) => println!("Succeeded with '{message}'"),Err(duration) => {println!("Failed after {} seconds", duration.as_secs())}}});
}

这样,我们就有了一个由另外两个异步帮助程序构建的工作超时。如果我们运行代码,它将在超时后打印失败模式:

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.pswp.cn/bicheng/84641.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2025年TCP洪水攻击防护实战全解析:从协议对抗到AI智能防御

一、2025年TCP洪水攻击的新特征与挑战 AI驱动的自适应攻击 攻击者利用生成式AI动态调整SYN报文特征&#xff08;如载荷内容、发送频率&#xff09;&#xff0c;使攻击流量与正常业务流量的差异率低至0.5%&#xff0c;传统基于规则引擎的防御策略完全失效。 混合协议打击常态化…

二、集成开发环境(IDE)

上节我们在终端演示了python虚拟环境的用法&#xff0c;但终端不方便代码编写和调试&#xff0c;本节介绍两种常用的python集成开发环境&#xff1a;Pycharm和Jupter Notebook。Pycharm是最流行的Python IDE&#xff0c;下载网址&#xff1a;下载 PyCharm&#xff1a;JetBrains…

芯片电感需求趋势及选型关键因素

随着AI产业的快速发展&#xff0c;数据中心、AI芯片、服务器等算力基础设施对于芯片电感等电子元件的要求不断提升。另一方面&#xff0c;电子产品向高功率密度和小型化方向发展&#xff0c;电源模块趋于小型化、低电压、大电流&#xff0c;也对芯片电感提出了小型化、轻量化、…

Vue3+Element Plus表单验证实战:从零实现用户管理

前言 在Vue3项目开发中&#xff0c;表单验证是保证数据完整性和准确性的重要环节。Element Plus作为Vue3的流行UI组件库&#xff0c;提供了强大的表单验证功能。本文将基于一个用户管理模块的实战案例&#xff0c;详细介绍Vue3中如何使用Element Plus实现完整的表单验证流程。…

力扣上C语言编程题:合并区间(涉及数组)

一. 简介 本文记录力扣网上涉及数组方面的编程题&#xff0c;主要以 C语言实现。 二. 力扣上C语言编程题&#xff1a;合并区间&#xff08;涉及数组&#xff09; 以数组 intervals 表示若干个区间的集合&#xff0c;其中单个区间为 intervals[i] [starti, endi] 。请你合并所…

SEO长尾词与关键词优化实战

内容概要 在SEO优化体系中&#xff0c;核心关键词与长尾词的协同作用直接影响流量获取效率与用户触达精度。本文将从基础概念切入&#xff0c;系统梳理核心关键词的筛选标准与竞争强度评估方法&#xff0c;并深入探讨长尾词在细分场景下的价值定位。通过分析用户搜索行为与意图…

博图SCL语言教程:灵活加、减计数制作自己的增减计数器(CTUD)

博图SCL语言教程&#xff1a;使用SCL实现增减计数器(CTUD) 一、什么是增减计数器(CTUD)&#xff1f; 增减计数器&#xff08;Up-Down Counter&#xff09;是PLC编程中的基础功能块&#xff0c;具有以下特性&#xff1a; CU (Count Up)&#xff1a;上升沿触发计数值增加 CD (…

Android 应用被kill问题排查和处理

一、背景 博主有一款应用市场应用,同样的应用,在Android 10上开启三个下载正常,在Android 14上开启下载安装,很频繁被kill。首先想到的是,是不是应用内存太高,导致被kill,通过工具分析内存也不高,后面就想到是不是系统本身分配给应用的内存就不高,后来通过排查,确实是和系统的…

从代码学习深度强化学习 - 多臂老虎机 PyTorch版

文章目录 前言创建多臂老虎机环境多臂老虎机算法基本框架(基类)1. ε-贪心算法 (Epsilon-Greedy)2. 随时间衰减的ε-贪婪算法 (Decaying ε-Greedy)3. 上置信界算法 (Upper Confidence Bound, UCB)4. 汤普森采样算法 (Thompson Sampling)总结前言 欢迎来到“从代码学习深度强化…

Android学习之Window窗口

Android Window机制学习笔记 在使用Window Flag实现界面全屏功能时&#xff0c;发现自身对Android Window机制缺乏系统认知&#xff0c;因此进行了专项学习与整理。 本文主要参考以下优质资料&#xff1a; Android的Window详解Android官方Window文档 Window基本概念 1. Win…

华为云 Flexus+DeepSeek 征文|搭建部署Dify-LLM推理引擎,赋能AI Agent智能体实现动态联网搜索能力

华为云 Flexus 云服务器 X 实例专门为 AI 应用场景设计。它提供了强大的计算能力&#xff0c;能够满足 DeepSeek 模型以及后续搭建 AI Agent 智能体过程中对于数据处理和模型运行的高要求。在网络方面&#xff0c;具备高速稳定的网络带宽&#xff0c;这对于需要频繁联网搜索信息…

Python 100个常用函数全面解析

Python 100个常用函数全面解析 1. 类型转换函数 1.1 int() 将字符串或数字转换为整数。 # 基本用法 int(123) # 123 int(3.14) # 3# 指定进制转换 int(1010, 2) # 10 (二进制转十进制) int(FF, 16) # 255 (十六进制转十进制)# 临界值处理 int() # ValueError: …

分享在日常开发中常用的ES6知识点【面试常考】

前言 在日常的业务开发中&#xff0c;可以熟悉运用掌握的知识点快速解决问题很重要。这篇分享JS相关的知识点&#xff0c;主要就是对数据的处理。 注意&#xff1a;本篇分享的知识点&#xff0c;只是起到一个抛砖引玉的作用&#xff0c;详情的使用和更多的ES6知识点还请参考官…

CHI协议验证中的异常及边界验证

CHI协议验证中的异常及边界验证 针对 CHI 协议的错误注入工具、覆盖率衡量方法及实际项目中的投入平衡 CHI 协议作为多核系统中复杂的缓存一致性协议,验证其行为需要强大的工具和方法来执行错误注入和边界条件测试,并衡量测试覆盖率。以下详细讨论常用工具、覆盖率评估方法及…

技术专栏|LLaMA家族——模型架构

LLaMA的模型架构与GPT相同&#xff0c;采用了Transformer中的因果解码器结构&#xff0c;并在此基础上进行了多项关键改进&#xff0c;以提升训练稳定性和模型性能。LLaMA的核心架构如图 3.14 所示&#xff0c;融合了后续提出的多种优化方法&#xff0c;这些方法也在其他模型&a…

电脑插入多块移动硬盘后经常出现卡顿和蓝屏

当电脑在插入多块移动硬盘后频繁出现卡顿和蓝屏问题时&#xff0c;可能涉及硬件资源冲突、驱动兼容性、供电不足或系统设置等多方面原因。以下是逐步排查和解决方案&#xff1a; 1. 检查电源供电问题 问题原因&#xff1a;多块移动硬盘同时运行可能导致USB接口供电不足&#x…

Go 语言实现高性能 EventBus 事件总线系统(含网络通信、微服务、并发异步实战)

前言 在现代微服务与事件驱动架构&#xff08;EDA&#xff09;中&#xff0c;事件总线&#xff08;EventBus&#xff09; 是实现模块解耦与系统异步处理的关键机制。 本文将以 Go 语言为基础&#xff0c;从零构建一个高性能、可扩展的事件总线系统&#xff0c;深入讲解&#…

npm ERR! @biomejs/biome@1.9.4 postinstall: `node scripts/postinstall.js`

npm install 报错如下, npm ERR! code ELIFECYCLE npm ERR! errno 1 npm ERR! @biomejs/biome@1.9.4 postinstall: `node scripts/postinstall.js` npm ERR! Exit status 1 npm ERR! npm ERR! Failed at the @biomejs/biome@1.9.4 postinstall script. npm ERR! This is pro…

APMPlus × veFaaS 一键开启函数服务性能监控,让函数运行全程可观测

资料来源&#xff1a;火山引擎-开发者社区 近年来&#xff0c;无服务器架构&#xff08;Serverless&#xff09;的崛起让开发者得以从基础设施的复杂性中解放&#xff0c;专注于业务逻辑创新。但随着采用率提升&#xff0c;新的问题开始出现——函数实例的短暂生命周期、动态变…

玛哈特零件矫平机:精密制造中的平整度守护者

在精密制造、模具、冲压、钣金加工、汽车零部件、航空航天以及电子设备等众多工业领域&#xff0c;零件的平整度&#xff08;Flatness&#xff09;是一项至关重要的质量指标。微小的翘曲、扭曲或弯曲都可能导致装配困难、功能失效、外观缺陷甚至影响整机性能。为了消除零件在加…