--- 摄于 2017 年 9 月 藏川线前段
随着 async/await mvp 语法的稳定,大量库在迁移使用 async/await 语法,那是不是手动实现 Future/Stream/Poll 就不存在使用的场景了呢?并不是的,任何东西都不会有完全分界的非黑即白。我们得知道这个语法糖是做了什么,然后才能理解为什么手动实现还存在必要性。这篇的观点主要由 https://github.com/nervosnetwork/tentacle/pull/249 这个 PR 引起。
我们首先要知道的是,async function 在编译器的作用下会展开为一个原始的 poll 模型状态机,一般来讲,大部分底层库,都会采用 poll + async 的写法来实现底层的一些逻辑,包括 futures/tokio/async-std,然后再尽可能暴露出可以直接使用 async/await 语法的上层接口,这样用户使用起来就更方便,但同时也会暴露一些约定的 poll 接口用来让可能的用户使用,作为上层库的底层包装,例如大多数类似 tcp 流的抽象都实现了 AsyncRead/AsyncWrite
,这样上层可以将它包装成一个带上层状态的流,同样实现 AsyncRead/AsyncWrite
,通过套娃的方式一层一层抽象给最终用户。
那么问题来了,async function 完全等价于手写的 poll 模式嘛?是否可以利用编译器生成的代码,将鸡生蛋蛋生鸡的的循环完成,实现真正的完全替换呢?答案非常明确,这是不行的。我们先不讨论一些额外的消耗,譬如多于手动实现的内存占用之类的东西,这些 overhead 如果可以接受,那就不是问题,真正导致它不行的原因是,状态机本身的状态保留问题。
接着,我们就要了解 async 生成的状态机到底长什么样,我再把 PR 里的代码贴过来:
async fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let len = self.socket.write(buf).await?; // ready here
self.socket.flush().await?; // pending here, but this buf is already write to inner socket, it will cause the caller to think that the send was not successful, and try to send again
ok(len)
}
fn poll_write(mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8]) -> Poll<io::Result<usize>> {
let task = pin_mut!(self.write(buf));
task.poll(cx)
}
为什么我会说这是状态机问题呢,我把 write function 生成的 Future 大致写出来:
pub struct Write<'a, Item> {
sink: &'a mut socket,
item: Option<Item>,
len: usize
}
impl<Item> Future for Write<'_, Item> {
type Output = Result<usize, io::Error>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = &mut *self;
if let Some(item) = this.item.take() {
let mut sink = Pin::new(&mut this.sink);
match sink.as_mut().poll_write(item)? {
Poll::Ready(size) => self.len = size,
Poll::Pending => {
this.item = Some(item);
return Poll::Pending;
}
}
}
ready!(Pin::new(&mut this.sink).poll_flush(cx))?;
Poll::Ready(Ok(self.len))
}
}
在这个 Future 运行的时候,如果 pending 返回,那这个任务就会被暂时挂起,并有对应的上下文保留,比如那个 Option<Item>
,当再次运行该 Future 的时候,可以从正确的状态上开始。
而如果在 poll function 里面使用如同 PR 的错误方式去实现,运行的状态是没有保留的,在 pending 的时候,直接退出函数,状态被清空,下次再进入该函数是一个新的 Future,这种做法会带来一些奇怪的问题,比如同一个操作进行了多次,如果不幂等,那直接就出现了奇怪的行为。
那么,是不是真的一点也不可以呢,其实,从理论的角度来说,如果你的 async function 里面只有一个需要 await 的地方, 或者任何操作在一个 async function 完成之前进行多次都是幂等的,那是可以这么做的,但事实上,涉及 io 的操作,绝大多数都不是幂等的,同时,如果使用别人实现的异步函数,任何人都无法保证内部的状态只有一个,所以,为了保证实现的正确,强烈建议禁用 async 实现 Poll,这是非常合理的建议。
这一篇就完结喽,这个 PR 的内部机制就这么点东西,没有更多啦
请登录后评论
评论区
加载更多