2.2. Future 特质

未匹配的标注

Future 特质

Future特质是Rust异步编程中的核心。Future是可以产生异步计算的值(尽管该值可以是空的,例如())。一个简化的Future 特质版本可能是这个样子:

trait SimpleFuture {
    type Output;
    fn poll(&mut self, wake: fn()) -> Poll<Self::Output>;
}
 enum Poll<T> {
    Ready(T),
    Pending,
}

通过调用该poll函数可以推进Futures,这将推动future尽可能地完成。如果Future完成,它将返回Poll::Ready(result)。如果Future尚未完成,它将返回Poll::Pending并安排在Future准备好进行更多进展时调用wake()函数。当wake()被调用时,executor(执行者)驱动Future将再次调用poll,以便Future能够取得更多进展。

如果没有wake()executor(执行者)将无法知道特定的Futures何时可以取得进展,并且必须不断地对每个Futures进行轮询。有了wake(),执行者确切地知道哪些future准备好poll

例如,考虑我们想要从可能已经或可能没有数据的套接字读取的情况。如果有数据,我们可以读取并返回Poll::Ready(data),但如果没有数据准备就绪,我们的Futures将被阻止,无法再进展。当没有数据可用时,我们必须注册wake在套接字上在准备好数据时进行调用,这将告诉执行者我们的Futures已准备好取得进展。

一个简单的SocketRead Futures可能看起来像这样:

struct SocketRead<'a> {
    socket: &'a Socket,
}

impl SimpleFuture for SocketRead<'_> {
    type Output = Vec<u8>;

    fn poll(&mut self, wake: fn()) -> Poll<Self::Output> {
        if self.socket.has_data_to_read() {
            // 套接字拥有数据就读取数据到缓冲区并返回数据.
            Poll::Ready(self.socket.read_buf())
        } else {
            // 套接字没有数据
            // 安排 `wake` 在有数据之后能够被调用.
            // 当数据可获得的时候, `wake` 将被调用 
            // 并且这个`Future` 的用户将知道再一次调用 `poll` 接收数据
            self.socket.set_readable_callback(wake);
            Poll::Pending
        }
    }
}

这种Futures模型允许将多个异步操作组合在一起,而无需中间分配。一次运行多个Futures或将Futures链接在一起可以通过无分配状态机实现,如下所示:

/// 一个SimpleFuture,可以同时运行另外两个future。
///
/// 并发是通过以下事实实现的:每个future都需要进行“poll”
/// 可能会交错,让每个future以自己的步伐前进。
pub struct Join<FutureA, FutureB> {
    // 每个字段都可能包含应运行以完成的future。
    // 如果future已经完成,则将该字段设置为“None”。
    // 这样可以防止我们在完成后轮询(poll)future。
    // 如果那样做就违反了`Future` 这个trait的契约(contract).
    a: Option<FutureA>,
    b: Option<FutureB>,
}

impl<FutureA, FutureB> SimpleFuture for Join<FutureA, FutureB>
where
    FutureA: SimpleFuture<Output = ()>,
    FutureB: SimpleFuture<Output = ()>,
{
    type Output = ();
    fn poll(&mut self, wake: fn()) -> Poll<Self::Output> {
        // 尝试完成 future `a`.
        if let Some(a) = &mut self.a {
            if let Poll::Ready(()) = a.poll(wake) {
                self.a.take();
            }
        }

        // 尝试完成 future `b`.
        if let Some(b) = &mut self.b {
            if let Poll::Ready(()) = b.poll(wake) {
                self.b.take();
            }
        }

        if self.a.is_none() && self.b.is_none() {
            // 所有的futures都完成了,我们可以成功的返回
            Poll::Ready(())
        } else {
            // 一个或者全部的futures返回了`Poll::Pending`,说明仍有工作需要去做
            // 他们将会调用'wake()',当取得进展时
            Poll::Pending
        }
    }
}

这展示了如何在不需要单独分配的情况下同时运行多个Future,从而允许更高效的异步程序。同样,多个顺序Future可以一个接一个地运行,如下所示:

///  SimpleFuture 将一个接一个地运行知道完成
//
// 注意: 为了这个简单的例子, `AndThenFut` 假设两个future在创建时可用
// `AndThen` 组合器允许基于输出的第一个future创建第二个future
// 像这样使用: `get_breakfast.and_then(|food| eat(food))`.
pub struct AndThenFut<FutureA, FutureB> {
    first: Option<FutureA>,
    second: FutureB,
}

impl<FutureA, FutureB> SimpleFuture for AndThenFut<FutureA, FutureB>
where
    FutureA: SimpleFuture<Output = ()>,
    FutureB: SimpleFuture<Output = ()>,
{
    type Output = ();
    fn poll(&mut self, wake: fn()) -> Poll<Self::Output> {
        if let Some(first) = &mut self.first {
            match first.poll(wake) {
                // 我们完成了第一个future,之后轮询它并完成第二个
                Poll::Ready(()) => self.first.take(),
                // 我们还不能完成第一个 future.
                Poll::Pending => return Poll::Pending,
            };
        }
        // 现在第一个future已经完成,尝试完成第二个
        self.second.poll(wake)
    }
}

这些示例展示了如何使用Future特征来表达异步控制流,而不需要多个已分配的对象和深度嵌套的回调。通过基本的控制流程,让我们来谈谈真正的Future特征以及它是如何不同的。

trait Future {
    type Output;
    fn poll(
        // 注意到这个从 `&mut self` 到 `Pin<&mut Self>`的更改:
        self: Pin<&mut Self>,
        // 也注意从 `wake: fn()` 到 `cx: &mut Context<'_>`的更改:
        cx: &mut Context<'_>,
    ) -> Poll<Self::Output>;
}

您将注意到的第一个更改是我们的self类型不再&mut self,但已更改为Pin<&mut Self>。我们将在后面的章节中详细讨论pinning,但现在知道它允许我们创建不可移动的Future。不可移动的对象可以在它们的字段之间存储指针,例如struct MyFut { a: i32, ptr_to_a: *const i32 }。此功能是启用async / await所必需的。

其次,wake: fn()已更改为&mut Context<'_>。在SimpleFuture,我们使用对函数指针(fn())的调用来告诉Future的执行者应该轮询相关的Future。但是,由于fn()它是零大小的,因此无法存储有关哪个task是唤醒了的数据。

在实际场景中,像Web服务器这样的复杂应用程序可能有数千个不同的连接,其唤醒应该分别进行管理。'Context' 类型通过提供对 'Waker' 类型值的访问来解决此问题,该值可用于唤醒特定任务。

本文章首发在 LearnKu.com 网站上。

上一篇 下一篇
贡献者:1
讨论数量: 0
发起讨论 只看当前版本


暂无话题~