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_将控制权_交回轮询它的运行时,允许运行时暂停它,并(如果必要)调度另一个任务进行执行,从而在多个方面同时取得进展。
我们将在后面的章节中回到放弃的重要性。