藏川线前段

--- 摄于 2017 年 9 月 藏川线前段

异步(六)

从上一讲,我们可以很清晰地发现,一个简单的 channel 实现,需要考虑的事情就有很多,这还不涉及业务,如果再将业务塞进来,这事情就比较难整了。

手写 Future 注意事项

正确实现 Future 是一件不太容易的事情,它需要理解 waker 机制,需要知道自己定义的 Future 执行需要依赖什么条件,自唤醒类型和叶子类型这两种 Future 在实现上是不一样的,当然它们也可以组合在一起,就更复杂了。

大体上,有一些注意事项需要考虑:

诸如此类的小心要点会有很多,一个个踩过去,太费时间了,想要大规模使用几乎很难搞定,需要将难度降下来,在真正需要手动操作的关键位置保留手动实现的可能性,其他地方用语法糖来简化,从而让易用性大大提高。

async/await

在引入 async/await 之前,Future 想要组合,只能选择组合子(combinator)或者手动将几个 Future 包装成一个大 Future 的方式去做。

组合子有两个问题:

  1. borrow check 和 owned data 造成的生命期问题
  2. 当组合过多之后,类型系统会复杂到无法表达

而手动包装,它需要一些前置条件,比如用户得清楚地了解并掌握之前几讲涉及的内容,尤其是 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/await 带来的麻烦

语法糖带来好处的同时,它也带来了不少麻烦。async in trait 这是个老大难问题,也不能算是语法的弊端,它只是在结合 trait 的时候,出现了更高的泛型需求,诸如生命期,借用检查等等问题。

真正的问题是, async fn 和 Future 并不完全等价,在一些情况下会出问题,尤其是中间状态的保存上,async fn 隐式生成的结构必须在未 Ready 的时候被保留,多次生成同一个结构并不完全相同,具体案例可以参考

最后

异步系列到这基本上就结束了,我想我应该已经回答了如下问题:

这个系列起源于我需要在公司内部分享 Rust 的异步生态,而 PPT 写不出来,通过文字叙述一遍找到思路之后,再去构建 PPT 我想应该会简单不少。

评论区

加载更多

登录后评论