使用 Rust 异步库通常是很容易的。除了要在各种地方写点 async 和 .await ,就像写普通的 Rust 代码一样。但当你编写自己的异步库的时候就会变得很困难。当我第一次尝试的时候,我对 T: ?Unpin 和 Pin<&mut Self> 感到非常困惑和难懂。我之前从未见过这些类型,我不能理解它们是用来做什么的。不过现在我理解它们了,我写了一个解释器,我希望我到时候可以回来阅读一下。在这篇文章中,我们将学习:
- 什么是
Future? - 什么是自引用类型
self-referential types? - 为什么它们是不安全的?
Pin / Unpin是如何让他们安全的?- 如何使用
Pin / Unpin去编写复杂嵌套的Future
什么是 Future ?
几年前,我需要编写一些需要异步执行的代码,并收集一些指标。例如,它花了多长的时间。我想要写一个 TimedWrapper ,它会像下面这样调用:
// Some async function, e.g. polling a URL with [https://docs.rs/reqwest]
// Remember, Rust functions do nothing until you .await them, so this isn't
// actually making a HTTP request yet.
let async_fn = reqwest::get("http://adamchalmers.com");
// Wrap the async function in my hypothetical wrapper.
let timed_async_fn = TimedWrapper::new(async_fn);
// Call the async function, which will send a HTTP request and time it.
let (resp, time) = timed_async_fn.await;
println!("Got a HTTP {} in {}ms", resp.unwrap().status(), time.as_millis())
我喜欢这个接口,它很简单,团队其他人应该也很容易去使用。让我们实现它吧!我们都知道在 Rust 底层 ,异步函数只是一个返回 Future 的常规函数。Future 这个 trait 非常简单,它意味着这个类型有以下特点:
- 可以被轮询
- 当它被轮询的时候,它应该会返回
Pending(待定)或者Ready(已准备) - 如果是
Pending, 应该过稍后再去轮询它 - 如果是
Ready,它将会携带一个响应值。我们就可以称它为Resolving(解决)
下面是一个简单的实现 Future 的例子,让它返回一个随机的 u16 类型。
use std::{future::Future, pin::Pin, task::Context}
/// A future which returns a random number when it resolves.
#[derive(Default)]
struct RandFuture;
impl Future for RandFuture {
// Every future has to specify what type of value it returns when it resolves.
// This particular future will return a u16.
type Output = u16;
// The `Future` trait has only one method, named "poll".
fn poll(self: Pin<&mut Self>, _cx: &mut Context) -> Poll<Self::Output> {
Poll::ready(rand::random())
}
}
好像并不难!我想我们可以准备实现 TimedWrapper 了。
初尝嵌套 Future 并在使用中栽跟头
让我们先定义一个类型。
pub struct TimedWrapper<Fut: Future> {
start: Option<Instant>,
future: Fut,
}
OK,所以 TimedWrapper 是一个具有 Fut: Future 的泛型类型。它将会存储一个 Future 作为一个字段,还有一个 start 字段用于记录它第一次被轮询的时间。让我们写一个构造函数。
impl<Fut: Future> TimedWrapper<Fut> {
pub fn new(future: Fut) -> Self {
Self { future, start: None }
}
}
这里没有太复杂的地方。new 方法接受一个 Future ,并把它包装在 TimedWrapper 中。当然,我们必须设置 start 为 None ,因为它还没有被轮询。所以让我们实现 poll (轮询)函数吧。我们要做的就是实现 Future 这个 trait,这样才让他可以使用 .await 。
impl<Fut: Future> Future for TimedWrapper<Fut> {
// This future will output a pair of values:
// 1. The value from the inner future
// 2. How long it took for the inner future to resolve
type Output = (Fut::Output, Duration);
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
// Call the inner poll, measuring how long it took.
let start = self.start.get_or_insert_with(Instant::now);
let inner_poll = self.future.poll(cx);
let elapsed = self.elapsed();
match inner_poll {
// The inner future needs more time, so this future needs more time too
Poll::Pending => Poll::Pending,
// Success!
Poll::Ready(output) => Poll::Ready((output, elapsed)),
}
}
}
OK,这里没有太难,但是有一个问题:它不能通过编译。
所以,Rust 编译器报告了一个错误在 self.future.poll(cx) 上,告诉我们在当前作用域中 Fut 上并没有 poll 这个方法。这让人疑惑,因为我们知道,Fut 是 Future 类型,所以他肯定有 poll 方法?但是编译器下面还有一句提示: Pin<&mut Fut> 里面有 poll 方法。所以这个奇怪的类型是什么?
好的,我们都知道方法里面有一个接收者 ”receiver",它可以操纵 self ,接收者可以是 self, &self, &mut self ,他们分别代表了获取 self 的所有权,self 的借用和 self 的可变借用。所以 Pin<&mut self> 是一个全新的,陌生的接收者。Rust 编译器解释了因为我们的 Fut ,所以我们需要 Pin<&mut self> 类型。在这里面有两个问题:
- 什么是
Pin? - 如果我们有一个类型 T, 要如何获取到
Pin<&mut T>?
剩下的文章就来回答这两个问题,我将解释 Rust 中可能导致不安全代码的一些问题,以及为什么 Pin 可以安全地解决这些问题。
自引用是不安全的
Pin 的存在是为了解决一些特定的问题:自引用数据类型,像数据结构中有指向他们自己的指针。举个例子,一个二叉查找树可能会自引用的指针,他们指向其他具有相同结构的结点。
自引用类型非常实用,但他们同时也很难保证内存安全。要了解原因,让我们用以下类型作为例子:两个字段,一个 i32 字段叫 val,和一个指向 i32 的指针叫 pointer。
到目前为止,一切正常。pointer 字段指向 val 字段的内存地址A,包含了一个合法的 i32类型数据。所有的指针都是合法的,他们指向的内存都编码了正确的值(在这个例子中是 i32 类型)。但是 Rust 编译器经常在内存中移动值(所有权转移)。例如,我们传入这个结构体进入到另一个方法中,它可能被移动到不同的内存地址中,或者我们应该用 Box 包装它,把它放在堆上。或者,如果这个结构体是在一个 Vec 中,然后我们插入了一些值进去,这个 Vec 可能会增加他的容量,这时就需要把它所有的元素移入一个新的,更大的缓冲区当中。
当我们移动它的时候,结构体中的字段就会改变他们的内存地址,而不是他们的值。所以 pointer 指针仍然指向之前的地址A,但地址A现在没有一个合法的 i32 值,之前地址A的数据已经被移动到地址B了,该地址可能被其他的值写入了!所以,现在 pointer 是非法指针。这很糟糕,最好的情况下非法指针会造成程序崩溃,最坏的情况下,他是一个可以被黑客利用的漏洞。我们只想要允许这些内存不安全的操作在 unsafe 代码块里,我们还要小心翼翼地为该类型写上注释来告诉使用者们当发生移动的时候要及时更新指针。
Unpin 和 !Unpin
为了回顾一下,所有的 Rust 类型可以归为两类:
- 在内存中移动是安全的。这是默认和规范情况下的,例如,包括像数字、字符串、 布尔值等原语,也包括完全由它们组成的结构体或枚举类型。大多数类型属于这一类!
- 在在内存中移动是不安全的,自引用类型。这是很罕见的,一个例子是 intrusive linked list inside some Tokio internals (一些 Tokio 内部结构中的侵入式链表),另一个例子是大多数实现
Future并借用数据的类型,原因在 Rust async book 中解释过。
在分类1中的类型在内存中的移动是完全安全的。你在移动它们的时候不会造成任何指针无效。但如果你移动了分类2中的类型,你就会让这些指针无效,还可能获得未定义的行为。正如之前我们看到的,早期的 Rust 版本中你必须非常仔细地使用这些类型,不去移动它们,或者你移动了它们之后使用 unsafe 更新所有的指针。但在 Rust 1.33 之后,编译器可以自动识别出所有类型是属于哪一类,并确保它们只能被安全地使用。
在分类1中的任何类型都实现了一个叫 Unpin 的特殊的 auto trait。非常奇怪的名字,但他的含义马上就会变得清晰。同样,大多数的普通类型实现了 Unpin ,因为他是一个 auto trait (就像 Send 、Sync 、 Sized 一样),所以你不用担心需要自己去实现它。如果你不确定一个类型是否可以被安全的移动,只需要在 docs.rs 查看它是否实现了 Unpin !
分类2中的类型,有个很有创意的名字 !Unpin ( 里面的! 意味着没有实现)。为了安全地使用这些类型,我们不可以使用常规的自引用指针。取而代之的是,我们使用特殊的指针将他们 ”固定“ 在某个地方,以确保它们不会被移动,这就是 Pin 这个类型的作用。
Pin 包装了一个指针并阻止了它的移动,唯一的例外就是如果这个值实现了 Unpin ,我们就知道了它可以被安全地移动。瞧!现在我们可以安全地写自引用类型结构体了!这是很重要的,因为在我们上面的讨论中,许多 Future 是自引用类型,而我们需要它们来实现 aysnc/.await 。
使用 Pin
现在我们理解了为什么有 Pin 的存在,和为什么我们的 Future 轮询函数(poll)的接收者是 Pin<&mut self> 而不是常规的 &mut self 。所以让我们回到我们刚才遇到的问题:内部的 Future 需要一个在内存中被固定的引用。
更一般地说:给定的一个被固定的结构体,我们怎么操作他的字段?
解决方法就是编写 helper 函数,为你提供对字段的引用。这些引用将会是像 &mut self 一样的普通引用,或者它们也可能被固定,你可以选择你需要的任意一种。这称为 投影(projection):如果您有一个固定结构体,您可以编写一个投影方法,让你可以访问其所有字段。
投影实际上只是将数据传入和传出 Pin。例如,我们从 Pin<&mut self> 中获取 start: Option<Duration> 字段,我们需要将 Future: Fut 放入 Pin 以便我们可以调用它的 poll 方法。如果你看过 Pin 的文档 你会知道如果指针指向的是 Unpin 的值,那么它将总是安全的,否则需要用 unsafe。
// Putting data into Pin
pub fn new <P: Deref<Target:Unpin>>(pointer: P) -> Pin<P>;
pub unsafe fn new_unchecked<P> (pointer: P) -> Pin<P>;
// Getting data from Pin
pub fn into_inner <P: Deref<Target: Unpin>>(pin: Pin<P>) -> P;
pub unsafe fn into_inner_unchecked<P> (pin: Pin<P>) -> P;
我知道不安全听起来有点吓人,但写 unsafe 代码还是能让人接受的。我认为 unsafe 是编译器在说 “嘿,我不知道这段代码是否符合这里的规则,所以我要依靠你来检查我。” Rust 编译器为我们做了很多工作,我们时不时地做一些工作才是公平的。 如果你想学习如何编写自己的投影方法,我强烈推荐这篇关于该主题的文章 fasterthanli.me/articles/pi…。 但我们现在要走一条捷径 : )
改用 pin-project
好吧,是时候坦白了,我不喜欢 unsafe 代码,我知道我刚解释了为什么可以用 unsafe ,但如果有得选的话,谁会用 unsafe 呢?i( ̄_, ̄ )
我开始写 Rust 并不是因为我想仔细考虑代码运行产生的后果,哈哈,我只是想快点且不去破坏东西。 幸运的是,有人同情我并写了一个可以生成完全安全投影的crate ! 它被称为 pin-project,它很棒。 我们需要做的就是在定义时加上一些宏:
#[pin_project::pin_project] // This generates a `project` method
pub struct TimedWrapper<Fut: Future> {
// For each field, we need to choose whether `project` returns an
// unpinned (&mut T) or pinned (Pin<&mut T>) reference to the field.
// By default, it assumes unpinned:
start: Option<Instant>,
// Opt into pinned references with this attribute:
#[pin]
future: Fut,
}
对于每个字段,您必须选择是否应固定其投影。 默认情况下,应该使用普通引用,因为它们更简单。 但是如果你知道你需要一个固定的引用。例如,因为你想调用 poll ,它的接收者是 Pin<&mut Self> ,那么你可以用 #[pin] 来实现。
现在,我们终于可以轮询内部的 Future 了!
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
// This returns a type with all the same fields, with all the same types,
// except that the fields defined with #[pin] will be pinned.
let mut this = self.project();
// Call the inner poll, measuring how long it took.
let start = this.start.get_or_insert_with(Instant::now);
let inner_poll = this.future.as_mut().poll(cx);
let elapsed = start.elapsed();
match inner_poll {
// The inner future needs more time, so this future needs more time too
Poll::Pending => Poll::Pending,
// Success!
Poll::Ready(output) => Poll::Ready((output, elapsed)),
}
}
最后,我们的目标完成了 —— 我们写完了所有的代码,并且没有使用任何的 unsafe 。
总结
如果 Rust 类型具有自引用指针,则无法安全地移动它。 毕竟,移动它们并不会更新指针,所以它们仍然指向旧的内存地址,所以它们现在是非法的。 Rust 可以自动判断哪些类型可以安全移动(并且会自动为它们实现 Unpin )。 如果你有一个指向某些数据的固定指针,Rust 可以保证不会发生任何不安全的事情(如果移动是安全的,你可以移动它,如果移动不安全,那么就会报错)。 这很重要,因为许多 Future 类型都是自引用的,所以我们需要 Pin 来安全地轮询 Future。 你可能不必自己轮询(只需使用 async/await),但如果您这样做,请使用 pin-project crate 来简化你的代码。