ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Rust 异步深入:运行时协作调度(yield_now)与自建 Future 抽象(timeout)——《The Rust Programming Language》ch17-03 实战指南

Rust 异步深入:运行时协作调度(yield_now)与自建 Future 抽象(timeout)——《The Rust Programming Language》ch17-03 实战指南 教程文档【免费下载链接】bookThe Rust Programming Language项目地址https://gitcode.com/gh_mirrors/bo/book点击查看免费下载本指南对应《The Rust Programming Language》第 17 章「Async and Await」的第三节More Futures仓库文档 src/ch17-03-more-futures.md。它解决两个核心问题其一当异步块内没有 await 点时Future 会独占运行时并饿死其他 Future需要掌握yield_now等协作让出手段其二如何用select与sleep两个现成积木组合出自定义的timeout抽象。读完本文你将理解 Rust 异步的协作式多任务模型并能独立编写可复用的 Future 组合工具。背景await 点是唯一的让出窗口在 “Our First Async Program” 一节中我们了解到在每一个 await 点上Rust 会给运行时一个机会——如果正在等待的 Future 尚未就绪运行时可以暂停当前任务并切换到其他任务。反过来同样成立Rust 只会在 await 点暂停异步块并把控制权交还运行时两个 await 点之间的所有代码都是同步执行的。这一特性意味着如果在一个异步块里做了大量工作却没有任何 await 点这个 Future 就会阻塞其他所有 Future 的推进。术语上这被称为某个 Future饿死starving其他 Future。有些场景下这无伤大雅但如果你在做昂贵的初始化、长时间运行的工作或者有一个会无限执行特定任务的 Future就必须认真思考在何时、何地交还控制权。用thread::sleep模拟阻塞型慢操作为了复现饥饿问题并寻找解法文档引入了一个slow函数见 listing-17-14 的源码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被用来替代现实中那些既耗时又阻塞的真实操作例如密集的 CPU 计算或同步 I/O。复现饥饿两个 Future 完全串行在 listing-17-15 的源码 中我们用slow模拟两个 Future 内的 CPU 密集工作并用trpl::select让它们竞争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::select(a, b).await;与之前用select竞争两个 URL 请求见 listing-17-05一样select仍然会在a完成时立即返回但两个 Future 内部的slow调用没有任何交错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.原因正是上文的原则aFuture 会一直做自己的工作直到遇到trpl::sleep的 await 点才把控制权交还随后bFuture 同样一路执行到自己的trpl::sleepawait 点最后a完成。要让两个 Future 在各自的慢任务之间交替推进就必须在中间插入 await 点——也就是说我们需要一个可以 await 的东西。插入trpl::sleep让任务交替推进listing-17-16 的源码 展示了最简单的解法在每次slow调用之后都追加一个trpl::sleep的 await 点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.); };现在两个 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.aFuture 在调用slow之后第一次 await 之前仍然先运行了一小段因为它在调用trpl::sleep之前就执行了slow但此后每当任意一个 Future 命中 await 点两者就来回切换。示例中我们在每次slow后都让出实际你可以按对自己最合理的粒度切分工作。用trpl::yield_now显式让出意图更清晰、开销更低上面用sleep其实并不理想——我们并不真的想睡觉而是想以最快速度推进只是需要交还控制权。为此 listing-17-17 的源码 把所有trpl::sleep替换为trpl::yield_nowlet 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.); };这段代码不仅更准确地表达了交还控制权的意图而且可能显著快于使用sleep定时器如sleep背后所用的那个往往有粒度下限。以本仓库使用的sleep为例即使传入 1 纳秒的Duration它也会至少休眠 1 毫秒。而现代计算机非常快——1 毫秒内足以做大量工作。源码印证trpl::yield_now来自 Tokio从 packages/trpl/src/lib.rs 可以看到trpl包对 Tokio 做了重导出task::{JoinHandle, spawn as spawn_task, yield_now}, time::{interval, sleep},也就是说trpl::yield_now就是 Tokio 的tokio::task::yield_now它会主动把当前任务放回调度队列尾端让出当前执行机会trpl::sleep则是tokio::time::sleep。这正是本仓库实践中一个 Future 饿死其他 Future的底层解药。协作式多任务让出是权利也是责任上述机制的本质是协作式多任务cooperative multitasking每个 Future 拥有何时通过 await 点交出控制权的决定权。因此每个 Future 也相应地有责任避免阻塞过久。在某些基于 Rust 的嵌入式操作系统中协作式调度甚至是唯一的多任务形式——这正是本节的实践意义所在。同时文档也给出清醒的工程提示真实代码中你通常不会每行都穿插 await 点。虽然这种让出相对廉价但并非免费很多情况下强行切分 CPU 密集任务反而会明显变慢此时让某个操作短暂阻塞可能对整体性能更有利务必要测量找到代码真正的性能瓶颈不过如果你看到本应并发的海量工作却在串行执行就要时刻想起await 点之外全是同步这一底层规律。自建异步抽象实现一个timeoutFuture 可以与其他 Future 组合从而构造出全新的模式。文档接下来引导我们基于已有的异步积木实现一个timeout函数——完成后它本身又成为构造更多异步抽象的积木。期望的用法Listing 17-18listing-17-18 的源码 展示了目标用法让一个需要 5 秒的慢 Future 与 2 秒的超时上限竞争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()) } }API 设计Listing 17-19在动手实现前先明确timeout的接口见 listing-17-19 的源码它本身必须是async fn这样调用方才能await它第一个参数是待运行的 Future用泛型F: Future约束使其适用于任意 Future第二个参数是最大等待时间用Duration便于直接传给trpl::sleep返回值是ResultFuture 成功完成则返回Ok(value)value 为 Future 的输出超时先到则返回Err(duration)即超时等待的时长。async fn timeoutF: Future( future_to_try: F, max_time: Duration, ) - ResultF::Output, Duration { // Here is where our implementation will go! }基于selectsleep的实现Listing 17-20行为层面我们需要让传入的 Future 与一个时长计时器竞争用trpl::sleep从Duration构造计时器 Future再用trpl::select让两者赛跑。listing-17-20 的源码 给出了完整实现use trpl::Either; async fn timeoutF: Future( future_to_try: F, max_time: Duration, ) - ResultF::Output, Duration { match trpl::select(future_to_try, trpl::sleep(max_time)).await { Either::Left(output) Ok(output), Either::Right(_) Err(max_time), } }把main中的用法与timeout定义合并即得到一个可完整运行的程序完整源码见 listing-17-20/src/main.rs其 Cargo.toml 以路径依赖引入trpl包edition 为 2024use std::time::Duration; use trpl::Either; async fn timeoutF: Future( future_to_try: F, max_time: Duration, ) - ResultF::Output, Duration { match trpl::select(future_to_try, trpl::sleep(max_time)).await { Either::Left(output) Ok(output), Either::Right(_) Err(max_time), } } fn main() { trpl::block_on(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()) } } }); }一个关键实现细节select并不公平文档特别指出trpl::select的实现不是公平的——它总是按参数传入的顺序轮询其他某些select实现会随机选择先轮询哪个参数。因此我们把future_to_try放在select的第一个参数位置让它即使面对极短的max_time也有机会先完成。匹配语义如下若future_to_try先完成select返回Either::Left(output)我们返回Ok(output)若计时器先耗尽select返回Either::Right(())我们忽略()用_绑定并返回Err(max_time)。运行上述代码会在超时后打印Failed after 2 seconds从 packages/trpl/src/lib.rs 的实现可以看到trpl::select的底层逻辑它基于futures::future::select构建内部用pin!固定两个 Future返回Either::Left(a)或Either::Right(b)并丢弃较慢的那个 Future。Either枚举本身来自futures包lib.rs 第 24 行 重导出futures::future::Either它与Result相似但不含成功/失败语义仅用Left/Right表示二者其一详见 ch17-01 中关于Either的讲解。抽象的组合timeout × retry × 网络调用因为 Future 可以与其他 Future 组合用较小的异步积木就能构建出非常强大的工具。文档给出了明确的延伸方向用同样的手法把timeout与重试retry组合再将两者应用于网络调用等真实操作例如 listing-17-05 中page_title这类请求。实践中的典型工作方式依然是直接用async/await编写主体逻辑辅以select这类函数与join!这类宏来控制最外层 Future 的执行方式。join!宏同样由trpl包从futures重导出见 packages/trpl/src/lib.rs。延伸阅读与仓库索引本节完整文档src/ch17-03-more-futures.md前一节Future 语法、select与Eithersrc/ch17-01-futures-and-syntax.md上一节并发应用、spawn_tasksrc/ch17-02-concurrency-with-async.md示例源码目录listings/ch17-async-awaitlisting-17-14 至 17-20 对应本文全部示例trpl教学支撑包实现packages/trpl/src/lib.rsblock_on在第 65 行、select在第 94 行trpl包依赖与特性声明packages/trpl/Cargo.toml基于futures、tokio、tokio-stream等本文所有示例均位于仓库的listings/ch17-async-await/目录下可直接用cargo run验证运行输出trpl包通过 Cargo 路径依赖挂接无需额外网络配置即可本地编译运行。赞分享教程文档【免费下载链接】bookThe Rust Programming Language项目地址https://gitcode.com/gh_mirrors/bo/book点击查看免费下载相关推荐Rust 异步编程权威指南async/await、Future 与 Stream 深度解析The Rust Programming Language 第 17 章Rust 异步编程权威指南async/await、Future 与 Stream 深度解析The Rust Programming Language 第 1教程文档Rust 入门第一步《The Rust Programming Language》第一章安装、Hello World 与 Cargo 实战指南Rust 入门第一步《The Rust Programming Language》第一章安装、Hello World 与 Cargo 实战指南 导读 本文以教程文档如何为老旧Intel Mac安装最新macOSOpenCore Legacy Patcher终极升级指南如何为老旧Intel Mac安装最新macOSOpenCore Legacy Patcher终极升级指南 OpenCore Legacy Patcher是一款操作系统固件驱动开发创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表