Rust异步3: async/await语法
上一篇Future模型中介绍了Future模式是如何工作的, 我们开发异步程序时只要实现Future, 然后交给runtime去推动就可以了. 虽然状态机实现的Future, 具有内存占用少, 执行效率高等优点, 但也存在一些问题, 比如:
- 异步代码要考虑让出和继续, 和同步代码的线性编程方式很不同
- 状态机的执行依赖于状态的变更, 程序越复杂, 状态维护越困难
- 需要经常和生命周期, Pin等晦涩的概念打交道, 编程门槛不低
为了缓解Future模式带来的复杂性, 也为了能和同步编程有相似的体验, Rust提供了async和await关键字, 下面我们就来看看这两个语法糖具体做了什么.
解开async语法糖
async用来标记一块代码是异步的, 一方面把异步和同步代码显式区分开来, 因为这两种代码的执行过程是不一样的; 另一方面告诉编译器, 这一块代码需要特别处理.
// 异步函数, a1 的类型是 impl Future<output=i32>
async fn f1() -> i32 { 1 };
let a1 = f1();
// 异步代码块, a2 也是 impl Future<output=i32>
let a2 = async { 1 };
// 异步闭包, a3 也是 impl Future<output=i32>
let f3 = async move || 1;
let a3 = f3();
使用async的地方有三个: 函数async fn
, 代码块async {}
和闭包async ||
. 这里函数与闭包一样, 被调用后返回一个Future, 而代码块本身是一个Future. async关键字对于这三者的作用是类似的, 下文主要介绍异步函数.
async fn foo() -> String { ... }
// 去掉async语法糖
fn foo() -> impl Future<Output=String> { ... }
async修改了异步函数的返回类型, 比如上面的foo函数, 返回类型String
只是内部类型, 外面还有一层隐藏的impl Future<...>
. 所以对于async fn foo() -> String { ... }
这个函数来说, 去掉async语法糖后就形如fn foo() -> impl Future<Output=String>
.
// 匿名的Future实现类型
enum _FooFuture { ... }
impl Future for _FooFuture {
type Output = String;
// 函数体转换成poll方法
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { ... }
}
}
// async fn foo() -> String { ... }
fn foo() -> _FooFuture { ... }
直接用Future来写异步代码的话, 我们首先得创建一个任务类型, 然后为这个类型实现Future, 还要和Pin, Waker这些打交道. 编写这样的代码是既繁琐又易出错, 要是能让开发者摆脱这些负担就好了. 正是出于这样的考虑, 编译器会为每个异步函数生成一个匿名的Future类型, 并且作为函数的实际返回类型.
async fn f1() -> String { return String::new(); }
async fn f2() -> String { return String::new(); }
assert_eq!(f1().type_id(), f2().type_id());
let futs = vec! [f1(), f2()]; // Error
这一方案减轻了开发者的心智负担, 但也带来了一些问题. 比如上面代码中, 看上去一样的两个返回类型, 实际却是不相同的, 把f1()
和f2()
直接放进集合的话会报错. 再比如还有怎么为返回的Future添加约束条件, 形如impl Future<...> + Send
这样的问题.
所以async关键字的作用在于屏蔽Future的复杂性, 可以编写async fn f() -> T
这样的异步函数, 代码中没有出现Future的身影. 开发者不需要了解底层的Future是什么样的, 不需要考虑poll该怎么实现, 也不需要和Pin, Waker这样的概念打交道, 很大程度上可以降低开发"上层"应用的难度.
解开await
语法糖
async fn web() {
receive().await;
// receive之后才会开始process
process().await;
}
async fn receive() {}
async fn process() {}
await是一种控制流程的语法. 对于并发来说, 多个任务之间是没有顺序关系的, 但单个任务本身还是有顺序的. 比如以web服务 接收请求 -> 中间处理 -> 返回响应 的例子来说, 虽然多个任务可以在线程上交替执行的, 但对于每一个任务, 中间处理 总要在 接收请求 之后. 任务会阻塞在await点上, 执行完await前的逻辑才会再执行后面的逻辑.
#[tokio::main]
async fn main() {
let fut = web();
let res = fut.await;
}
await表示这个任务需要执行. 调用异步函数只是创建了一个Future实例, 而poll方法对于的业务逻辑并不会执行. Future的这种惰性, 正需要await来推动. 比如上面的代码, 如果没有.await, 那就只是在内存中创建了一个变量, 没有用就销毁掉了, Future里的逻辑没有执行.
enum _WebFuture { RECEIVE, PROCESS, END }
impl Future for _WebFuture {
type Output = ();
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
return loop {
match & *self {
// 子过程1: 接收请求
_WebFuture::RECEIVE => {
*(self.as_mut().get_mut()) = _WebFuture::PROCESS;
break Poll::Pending;
}
// 子过程2: 中间处理
_WebFuture::PROCESS => {
*(self.as_mut().get_mut()) = _WebFuture::END;
break Poll::Ready(());
}
// 子过程3: 返回响应
_WebFuture::END => { panic!(...); }
}
}
}
}
async是任务让出执行与继续执行的点. 对于异步任务来说, await点是要等待事件的地方, 也就是前面文章里提到的任务被分割成子过程的地方, 所以对于poll方法来说, await点就要考虑让出与继续执行的点. 上文说到编译器会为异步函数生成匿名的Future实现, 而生成的poll方法是原来函数体的转换, 函数体里的await点对应poll里return和match的地方.
所以await关键字, 对于编译器来说, 提供了异步任务中的"断点"信息, 使得异步函数到Future的转换成为可能; 对于开发者来说, 提供了流程控制, 使得异步代码像同步代码一样, 可以用顺序的方式编写.