--- 摄于 2017 年 9 月 藏川线前段
从上一讲,我们可以很清晰地发现,一个简单的 channel 实现,需要考虑的事情就有很多,这还不涉及业务,如果再将业务塞进来,这事情就比较难整了。
正确实现 Future 是一件不太容易的事情,它需要理解 waker 机制,需要知道自己定义的 Future 执行需要依赖什么条件,自唤醒类型和叶子类型这两种 Future 在实现上是不一样的,当然它们也可以组合在一起,就更复杂了。
大体上,有一些注意事项需要考虑:
诸如此类的小心要点会有很多,一个个踩过去,太费时间了,想要大规模使用几乎很难搞定,需要将难度降下来,在真正需要手动操作的关键位置保留手动实现的可能性,其他地方用语法糖来简化,从而让易用性大大提高。
在引入 async/await 之前,Future 想要组合,只能选择组合子(combinator)或者手动将几个 Future 包装成一个大 Future 的方式去做。
组合子有两个问题:
而手动包装,它需要一些前置条件,比如用户得清楚地了解并掌握之前几讲涉及的内容,尤其是 waker 这套机制,并且由于最终 Future 是由多个 Future 合并而成的,它被唤醒的时候,waker 中并没有传递是其中哪个 子 Future 的唤醒,于是任意唤醒行为都需要遍历整个大 Future 的所有内容,否则就有可能造成比较严重的延时甚至完全丢失信号,导致 Future 无法正常工作。
async/await 语法极大简化了用户的使用门槛,它让各种 Future 在组合的时候能够更加直观地看到对应的行为和依赖,形如:
enum State {
Start(B, Next(do_something)),
Next(C),
None,
}
struct Example {
inner: State
}
impl Future for Example {
type Output = ();
fn poll(self: Pin<&mut self>, cx: Context<'_'>) -> Self::Output {
let mut inner = replace(&mut self.inner, State::None);
loop {
match inner.as_mut {
Start(f, Next(t)) => {
match f.poll(cx) => {
Ready(a) => inner = State::Next(t(a)),
Pending => {
self.inner = inner;
return Pending
}
}
}
State::Next(t) => {
match t.poll(cx) => {
Ready(_) => return Ready(()),
Pending => {
self.inner = inner;
return Pending
}
}
}
}
}
}
}
可以直接简化为:
async fn example() {
let a = b.await;
do_something(a).await;
}
手写 Future 状态机相对来说,控制流上的表达并不是很直观,要让异步作为日常开发工具之一,那必须让它的易用度(使用门槛)下降到一定级别,哪怕这种下降的背后是复杂度极高的代码生成工作。
我们知道,运行时真正认识的是 Future,那么 async fn
它的作用就是生成一个 impl Future
匿名结构,里面的 .await
先变成 struct GenFuture<T: Generator<ResumeTy, Yield = ()>>(T)
然后再转换成 Future,这样就将复杂的工作交给了编译器,用户可以更轻松地使用异步了。
语法糖带来好处的同时,它也带来了不少麻烦。async in trait
这是个老大难问题,也不能算是语法的弊端,它只是在结合 trait 的时候,出现了更高的泛型需求,诸如生命期,借用检查等等问题。
真正的问题是, async fn
和 Future 并不完全等价,在一些情况下会出问题,尤其是中间状态的保存上,async fn
隐式生成的结构必须在未 Ready 的时候被保留,多次生成同一个结构并不完全相同,具体案例可以参考。
异步系列到这基本上就结束了,我想我应该已经回答了如下问题:
这个系列起源于我需要在公司内部分享 Rust 的异步生态,而 PPT 写不出来,通过文字叙述一遍找到思路之后,再去构建 PPT 我想应该会简单不少。
请登录后评论
评论区
加载更多