10.3 异步编程:async与await


10.3 异步编程:async 与 await

本节摘要:async 函数编译成状态机,await 是"此处可让出"的挂起点;少量线程由运行时调度成千上万的任务,等待不再占用线程。本节讲 async/await 的本质、运行时的分工与 Future 契约,并对比第 8 章的线程路线。读完你能写基本的异步代码并理解它为什么便宜。

线程模型的账本

第 8 章每线程一条 OS 线程:每条栈预留兆级内存、切换进内核。一万并发连接就是一万条线程——账算不平。异步的思路:等待让出线程,谁就绪谁上。任务挂起时栈被压进堆上的状态机,内存开销以百字节计。

async 与 await 的语法

async fn fetch_archive(no: u32) -> String { let meta = fetch_meta(no).await; // 挂起点:等待时让出线程 let body = fetch_body(meta).await; format!("{}{}", meta, body) }

两个关键词的判决:

  • async fn 把函数体编译成一个实现 Future 契约的状态机,不立即执行;
  • .await 等待另一个 Future 完成,期间把当前线程让给其他任务。

Rust 的异步与多数语言有一个关键差异:语言只提供机制,调度交给运行时库。std 不带运行时,工程里用 tokio 或 async-std 等执行器:#[tokio::main] 宏把 main 变成运行时入口。

#[tokio::main] async fn main() { let a = fetch_archive(173); let b = fetch_archive(174); let (ra, rb) = tokio::join!(a, b); // 并发执行,不是顺序 println!("{} / {}", ra, rb); }

Future 契约

Future 只有一个方法 poll:运行时调用它问"好了吗",好了交值,没好交出"什么时候再问"的唤醒凭据(Waker)。整个异步生态建立在这一个小契约上——第 5 章 Trait 契约思想的极致体现。

图:线程模型与任务模型对照

图:线程模型与任务模型对照

要点回顾

  • async 是状态机、await 是挂起点,两个词合起来构成让出式并发;
  • 运行时由库提供,语言与调度解耦是 Rust 异步的独特结构;
  • join 宏并发多个 Future,逐个 await 则退化为顺序;
  • IO 密集选异步、CPU 密集选线程,两条路线互补而非替代。

async fn 的展开:一台状态机

async fn 编译成一台状态机,每个 .await 是一个暂停点——保存现场、交还控制权、就绪后恢复。理解这一点,异步的全部怪象(Pin、不能递归、阻塞危害)都有了解释。

use std::time::Duration; async fn fetch_page(id: u32) -> String { std::thread::sleep(Duration::from_millis(10)); // 演示用:真实代码是异步 IO format!("档案{}内容", id) } async fn audit(ids: &[u32]) -> u32 { let mut hits = 0; for id in ids { let text = fetch_page(*id).await; // 暂停点:让出执行权 if text.contains("违规") { hits += 1; } } hits } fn main() { let ids = [1u32, 2, 3]; let fut = audit(&ids); // 不执行:只生成状态机 let result = futures_executor_block_on(fut); // 阻塞驱动(示意) println!("命中 {}", result); } fn futures_executor_block_on<F: std::future::Future<Output = u32>>(f: F) -> u32 { // 最小执行器示意:真实执行器由 tokio 等运行时提供 let mut f = Box::pin(f); let waker = noop_waker(); let mut cx = std::task::Context::from_waker(&waker); loop { match f.as_mut().poll(&mut cx) { std::task::Poll::Ready(v) => return v, std::task::Poll::Pending => std::thread::sleep(Duration::from_millis(1)), } } } fn noop_waker() -> std::task::Waker { use std::sync::Arc; struct N; impl std::task::Wake for N { fn wake(self: Arc<Self>) {} } std::task::Waker::from(Arc::new(N)) }

末尾的手工执行器刻意裸露了异步的契约:Future 只有被 poll 才前进,waker 是"就绪了叫我"的回执。生产中绝不手写执行器,这段是解剖标本。

阻塞:异步法庭的头号罪

在异步任务里做阻塞调用(thread::sleep、同步 IO、重计算),执行器的线程被钉死,同线程其他任务全部陪葬——症状是延迟毛刺,而不是报错。纪律:异步上下文内只做 .await;必须阻塞时用 spawn_blocking 一类设施移出执行线程;CPU 密集任务另起线程或进程。

async 的选型表

维度 线程 异步任务
切换成本 内核级,微秒级 用户态,纳秒级
数量级 数百 数十万
阻塞容忍 各自独立 一损俱损
适合 CPU 密集、低并发 海量连接、IO 密集

万级以下并发用线程通常更简单;异步的收益在连接数上万、单连接工作轻的 IO 场景才真正兑现。选型先于优化,用错模型的异步比线程更慢更难查。

三条收尾纪律

异步投产前三问:所有阻塞点是否已识别并移出执行线程;任务取消语义是否明确(.await 中途被丢弃时资源如何释放);超时是否外置包裹而非依赖内部 sleep。第二条最常缺席——默认执行器丢弃 future 时不会执行清理魔法,需要 RAII 守卫兜底。把三问写进异步模块的 PR 模板,毛刺类线上问题的大半可以在合并前拦下。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U