Future特性

局部Rc问题

回到tokio::spawn的签名:

#![allow(unused)]
fn main() {
pub fn spawn<F>(future: F) -> JoinHandle<F::Output>
    where
        F: Future + Send + 'static,
        F::Output: Send + 'static,
{ /* */ }
}

对于F来说,Send实际上意味着什么呢?正如我们在前一节中看到的,这意味着它从派生环境中捕获的任何值都必须是Send。但不仅如此。

任何_跨过.await点_持有的值都必须是Send。让我们看一个例子:

#![allow(unused)]
fn main() {
use std::rc::Rc;
use tokio::task::yield_now;

fn spawner() {
    tokio::spawn(example());
}

async fn example() {
    // 一个不是`Send`的值,在异步函数内部创建
    let non_send = Rc::new(1);
    
    // 一个什么都不做的`.await`点
    yield_now().await;

    // `.await`之后仍然需要局部非`Send`值
    println!("{}", non_send);
}
}

编译器会拒绝这段代码:

错误:future不能安全地在线程间发送
    |
5   |     tokio::spawn(example());
    |                  ^^^^^^^^^ future由`example`返回的不是`Send`
    |
注意:future不是`Send`,因为此值在线程等待中被使用
    |
11  |     let non_send = Rc::new(1);
    |         -------- 类型为`Rc<i32>`,不是`Send`
12  |     // 一个`.await`点
13  |     yield_now().await;
    |                 ^^^^^ 等待发生在这里,`non_send`可能稍后使用
注意:`tokio::spawn`中所需的一个约束
    |
164 |     pub fn spawn<F>(future: F) -> JoinHandle<F::Output>
    |            ----- 此函数所需的一个约束
165 |     where
166 |         F: Future + Send + 'static,
    |                     ^^^^ 此约束在`spawn`中所需

为了理解为什么会这样,我们需要深化对Rust异步模型的理解。

Future特性

我们之前说过,async函数返回future,实现Future特性的类型。你可以将future视为一个状态机。它处于以下两种状态之一:

  • pending:计算尚未完成。
  • ready:计算已完成,这里是输出结果。

这一点在特性定义中编码如下:

#![allow(unused)]
fn main() {
trait Future {
    type Output;
    
    // 目前忽略`Pin`和`Context`
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
}

poll

poll方法是Future特性的核心。future本身不做任何事情。它需要轮询才能取得进展。当你调用poll时,你是在要求future做一些工作。poll尝试取得进展,然后返回以下之一:

  • Poll::Pending:future还没有准备好。你需要稍后再调用poll
  • Poll::Ready(value):future已经完成。value是计算的结果,类型为Self::Output

一旦Future::poll返回Poll::Ready,就不应再对其进行轮询:future已完成,没有更多事情要做。

运行时的角色

你很少(如果有的话)会直接调用poll。这是你的异步运行时的工作:它拥有poll签名中所需的全部信息(Context),以确保你的future能在可能时取得进展。

async fn和future

我们一直使用的是高层接口,即异步函数。现在我们已经查看了低层原始特性,即Future特性。

它们是如何关联的呢?

每次你将一个函数标记为异步时,该函数都会返回一个future。编译器会将你异步函数的主体转换为一个状态机:每个.await点对应一个状态。

回到我们的Rc例子:

#![allow(unused)]
fn main() {
use std::rc::Rc;
use tokio::task::yield_now;

async fn example() {
    let non_send = Rc::new(1);
    yield_now().await;
    println!("{}", non_send);
}
}

编译器会将其转换为类似这样的枚举:

#![allow(unused)]
fn main() {
pub enum ExampleFuture {
    NotStarted,
    YieldNow(Rc<i32>),
    Terminated,
}
}

当调用example时,它返回ExampleFuture::NotStarted。future从未被轮询过,所以什么也没发生。 当运行时首次轮询它时,ExampleFuture会前进到下一个.await点:它会在状态机的ExampleFuture::YieldNow(Rc<i32>)阶段停止,并返回Poll::Pending。 当再次轮询时,它将执行剩余的代码(println!)并返回Poll::Ready(())

当你查看它的状态机表示形式ExampleFuture时,现在就很清楚为什么example不是Send了:它持有一个Rc,因此不能是Send

放弃点

正如你刚在example中看到的,每一个.await点都会在future的生命周期中创建一个新的中间状态。 这就是为什么.await点也被称为yield points:你的future_将控制权_交回轮询它的运行时,允许运行时暂停它,并(如果必要)调度另一个任务进行执行,从而在多个方面同时取得进展。

我们将在后面的章节中回到放弃的重要性。