一句话总结
async 把一个函数/代码块变成"返回 Future 的东西";.await 则表示"在这里等这个 Future 完成,期间让出执行权".
编译器自动把顺序代码翻译成上一篇那种状态机.代价是 async 会沿调用链"传染"--这就是所谓的函数染色.
从手写 Future 到 async
上一篇我们看到 Future 是个状态机.如果每个异步任务都要手写 poll 和状态转换,没人受得了.async/await 就是为此而生的语法糖:用近乎同步的顺序写法表达逻辑,编译器替我们生成状态机.
|
这段代码读起来是"连接→查询→返回"的直线,但编译器会把它编译成一个含三个状态(等连接,等查询,完成)的 Future--正是上一篇手画的那种状态机,只是不用手动实现.
async:制造 Future
async 可以修饰函数,也可以包裹一个代码块.它的效果统一是:把里面的代码包装成一个 Future,而不是立即执行.
|
写 async fn greet() -> String,它的真实返回类型不是 String,而是 impl Future<Output = String>.
也就是说:声明的 -> String 指的是"这个 Future 完成后产出 String",函数本身返回的是 Future.呼应上一篇:调用 greet() 只是造出 Future,函数体一行都还没跑.
await:推进并等待
.await 只能用在 async 上下文里.它的语义是:开始 poll 这个 Future;如果就绪,取出结果继续;如果未就绪(Pending),就把当前任务让出去,等被唤醒后从这里恢复.
|
.await 是异步代码里的"礼让点":跑到这里,如果要等,当前任务主动靠边停车,让后面的车(其他任务)先过;等自己要的东西就绪了,再重新汇入车流,从停车点继续开.
两个 .await 之间的代码则是连续执行,不会被打断的.
编译器做了什么:还原状态机
把前面的 fetch_user "脑内编译"一遍,async 的本质就彻底清晰了.编译器大致这样转换:
|
规律很清晰:每个 .await 就是状态机的一个暂停点..await 之间的局部变量(如 conn)需要跨暂停点存活,于是被编译器塞进 Future 结构体里保存--这解释了上一篇说的"现场保存在 Future 里".
这也解释了 Pin 为何存在
既然局部变量被搬进 Future 结构体,而它们之间可能存在引用(一个变量借用另一个),这个 Future 就成了自引用结构.一旦它在内存里被移动,内部引用就会指向错误地址.
Pin 的职责正是"钉住它别移动",保护这些自引用.现在这条线索闭环了.
函数染色:async 会传染
有一个绕不开的特性必须正视:.await 只能在 async 函数/块里使用.这意味着,一旦某个底层函数是 async 的,所有想 await 它的调用者也必须是 async,层层向上传染.社区称之为"函数颜色(function color)"问题--函数被染成了"同步色"和"异步色"两种,异步色会顺着调用链蔓延.
|
同步函数和异步函数像两种插头:异步插头插不进同步插座.不能在普通 fn 里直接 .await,正如不能拿三脚插头硬插两孔插座--需要一个"转换器",而那个转换器就是运行时(下一篇)提供的入口,比如 #[tokio::main] 或 block_on.
Go 没有这个问题,但代价不同
Go 里任何函数都能直接发起"阻塞"调用,无需染色--因为 runtime 内建,处处可挂起.Rust 选择不内建运行时(回顾第 01 篇的理由),代价就是 async 染色.
好在实践中,异步项目通常"一异步到底",染色带来的摩擦比初看时小.
异步世界的入口:总得有人 await 最外层
染色推到最顶层会遇到一个问题:main 该怎么办?main 默认是同步的,但我们需要在它里面 await 最外层的 Future.这就需要运行时提供一个"同步 → 异步"的桥:
|
#[tokio::main] 在背后创建运行时,并用它 block_on 我们的 async main.这个"运行时到底是什么,怎么驱动 Future"的问题,正是下一篇的主题.
快速回顾
- async:把函数/代码块包装成 Future;
async fn () -> T真实返回impl Future<Output = T>,调用时不执行函数体. - .await:poll 一个 Future,就绪则取值继续,未就绪则让出当前任务,被唤醒后从此处恢复.
- 编译器生成状态机:每个
.await是一个暂停点;跨暂停点的局部变量被存进 Future 结构体. - Pin 闭环:被保存的变量间引用使 Future 成为自引用结构,故需 Pin 防移动.
- 函数染色:
.await只能在 async 上下文用,async 沿调用链传染;Go 因内建运行时无此问题. - 入口桥:
#[tokio::main]/block_on把同步 main 接入异步世界.
动手练习
- 真实返回类型:写一个
async fn add(a: i32, b: i32) -> i32,说出它的真实返回类型是什么. - 复现报错:在普通
fn里调用一个 async 函数并.await,记录编译错误,再用#[tokio::main]修复. - 数暂停点:给一个含两个
.await的 async 函数,数出它的状态机有几个暂停点. - 理解 Pin:解释为什么跨
.await存活的局部变量会被放进 Future 结构体,这和 Pin 有何关系. - 函数颜色:用"函数颜色"概念,说明为什么一个底层 async 函数会迫使它的所有调用者也变成 async.