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